Introduction - 'Click Start VDN'

 Top Next

The following text is designed to be of help to anyone starting to learn programming in .NET using the new Vulcan language from Grafxsoft.

All research has been done by Dr Phil Hepburn with much help from his professional colleagues.

Special thanks must go to Chris Pyrgas, Paul Piko, John Parker, Colin Newell, and Alwyn Jennings-Bramly for their help and support - plus a handful or two of good ideas.

Don't forget to keep visiting your planetary home base at :- http://www.govulcan.net/ just to see what is going on and new to help you develop.

Recommendations :

I highly recommend getting any, and all, of Paul's publications and conference notes. Keep looking at his web site too.

His first Vulcan book is "Vulcan.NET At Warp Speed" - available from:- the official Vulcan site: http://www.govulcan.net

Also Paul's information site, Vulcan Mind Meld:- http://www.vulcanmindmeld.net - his home page is shown below.

He has some exciting information coming on WPF and Drag and Drop I believe. Keep checking out his sight !

PHH November 2007.


Welcome note

 Top Previous Next

I hope you enjoy working through some or all of the example topic on offer in "Click-Start-VDN" as much as I have enjoyed my experience in putting together the material on offer to you.

The general philosophy behind the body of this work is that prospective Vulcaner from the programming community at large, will really only need samples of working code to get them on their way.

If the working code fits into a developing sequence, and also covers most of the basic areas required to code solutions to most business problems, then it will do the job expected.

This work is not designed to address object programming, or scoping of variables, and the like. However, you may pick up a few tips from just studying the code samples as you go along.

Good Luck with your journey into Vulcan and .NET.

Dr. Philip H. Hepburn - BEST consultancy - UK

November 2007


Introduction

It is not intended that a reader follows the sequence of chapters and sub-sections from start to finish.

It is recognised that many readers will probably have a good working knowledge of Visual Objects when they approach learning Vulcan.NET (VDN).

The overall design is such that each small section will make reasonable sense on its own. So that code may be copied, and adapted, directly into a user application.

Out of interest, you may like to know that I refer to it myself, just to see the correct form and syntax of the code required to do a certain task.

In early days I had Chris Pyrgas's notes from DevFest to get me going, but unfortunately they were not as extensive as these. Chris has helped me a great deal, and so I am trying to pass on something to my colleagues in the new Vulcan community in return.

Note the sections are not in any particular order so just dip into the text where you think you are interested most, have a read and try to follow the examples in VIDE, or any other IDE of your choice.

I am aware that the images of code and output make copy and paste impossible so I will include sub-sections within each section with just coded text to help you get places faster. However, typing in the code does help to reinforce what you are trying to learn.


IMPORTANT changes at release

Not using USING :

In this short sub-section I will inform you of changes have taken place during the preparation of the body of this work, and those which will affect how you need to write your code and operate the IDE of your choice.

The most obvious of these is the change from USING to #using before your PRG code sections.

Most of what will be seen in the images of the live code with over-laid live output, shows 'USING'.

In mid February 2008 Don informed the community that there is a need for all of us to change to #using, or #USING.

Basically, from my own point of view, I have just started to insert a # before the lines of code I have in my VIDE editor already.

Where possible I will try and update Full Working Code sections to reflect this need to change.

From Vulcan version 147 this is to be supported, and soon after that the USING without the # will be dropped. So you may as well get used to this ASAP :-)

The Escape Sequence :

Other recent changes include the addition of an 'e' before strings in our code. Plenty of comments and explanation appeared in the News Groups, including why etc. etc.

However, here we will try just to focus on what to do - to get all related stuff to work.

Let us look at an example of a code line recently done for the ZedGraph section :-

myText := TextObj{ e"\"Interesting\"\nPoint", 45.0,7.85}

The 'e' before the string means interpret 'certain characters' between the double quotes in a certain way. I will explain what the special characters are below.

Note that if you do not have any special characters enclosed in the quotes then you may drop the 'e' as it is therefore not required.

The most obvious character which would gives us grief is " the double quote itself. If we wish to have this printed out, or seen as just part of the string itself, then we need to precede it with a backslash or \ which means it can be interpreted according the the 'simple' escape sequence 'rules'.

So put simply, if we wish to have any of the special characters interpreted as themselves we need to precede them with a \ and also have an 'e' before the string itself.

The resulting 'pairs' of characters are as follows :-

\' \" \? \\

\a \b \f \n \r \t \v \0

Obviously, the first line simply results in the second character itself being shown.

However, the second line pairs result in special actions / results which as as follows :-

a.) is for alert - or a bell ring (ASCII 7)

b.) gives a backspace (ASCII 8)

f.) results in a form feed (ASCII 12)

n.) a new line (ASCII 10)

r.) gives a carriage return (ASCII 13)

t.) a horizontal tab (ASCII 9)

v.) a vertical tab (ASCII 11)

0.) a Null Character (ASCII zero)

Consult the SDK if you are interested in Octal or Hexadecimal.

If you wish to see this in action then look at the code in section 15 for the conversion of DBF data into CSV files.

A relevant part of which is shown below :-

LOCAL cRet, cDQ, cSQ AS STRING

LOCAL nMonth AS LONG

cDQ := e"\""

cSQ := e"\'"

DO CASE

CASE cDType == "C"

cRet := uVal

cRet := ALLTRIM(cRet)

cRet := StrTran(cRet, cDQ, cSQ) // Replace double with single quotes

cRet := cDQ+cRet+cDQ // Add quotes

I apologise for the old style VO function string manipulation and leave it as an exercise for you to convert this using methods of the String class. If you are not certain what I mean then find a way to remove the 'AllTrim' and 'StrTran' functions.

PHH Feb. 2008


Section 0 - VIDE

VIDE is an Interactive Development Environment (IDE) for Vulcan.NET and has been written in Vulcan itself by Chris Pyrgas.

I would like to make it clear from the outset that I have learned all the dotNet I know using Vulcan syntax and Chris's VIDE.

Once I got going writing small applets in Vulcan I found the VIDE very easy to use and it did not get in the way of my coding and design at all. With some IDEs you feel that you are spending a great deal of energy, time, and mental concentration 'managing' the IDE itself. This is not true with VIDE, in fact it helps your learning, without you realising it at first.

The intellisense built into VIDE is also very helpful to all, not just beginners to Vulcan. Once I got into the swing I used intellisense to learn more of what was in dotNet.

The developer coming to Vulcan from Visual Objects gets a extra few bonuses, as many features are familiar after using the VO Repro for years.

Overall, I found that I could put 100% of my efforts into coming to terms with dotNet and get success with my coding quickly. Once you are proficient with VDN then you will be able to move over to Visual Studio and handle that environment better than trying to do everything at once. I intend to do just that in order to access other dotNet languages.

What I particularly like about VIDE is that I can have one instance open on one monitor, and another completely different instance on the other. So I can do all sorts of edit, compile and run at one while doing something completely different on the other - not much help to real beginners, but helpful when doing notes like this.

As an application it feels "lean and mean" - which is a complement in boxing parlance I believe !

If you have multiple monitors then the toolbox and properties window will "float" onto the other screen, giving plenty of room for development.

I must admit I ought to re-visit the help in VIDE to see that I am getting the most from it, I have a tendency to just get on and use it - which is probably a reflection on how easy it is to use. Trouble is I have been doing this from day two!

Well done Chris, this is a great project and product to have created. I am sure many will enjoy the fruits of your labours.

{ this section is dedicated to Don, to prove that in the real world we do have chapter zero, and house number zero, and the like ! - hope this arrayses the memory !! }


Section 1 - GUI form controls (I)

Enter topic text here.


Form

System.Windows.Forms.Form class :-

I have not got a lot to say about GUI at a Form level - yet. Done all my development work with just one or two interrelating forms.

You can do a lot of interesting stuff to get you going in Vulcan and dotNet without needing to know much more than how to open a form in a modal, or non-modal fashion, and also how to prevent resizing of the form by the user.

Both of these are covered in General Tips - Section 29.

It has to be realised by the developer / coder that events for the GUI take place at a control level, much more than with the VO windows. Here we are really discussing combo and list box selection, as well as radio button 'checking'. Yes, we "check" and not "press"!

So far I have done a lot with Win Forms and focused my attention on handling the events associated with the individual controls.

Go and study the information about the RadioButton class, later in this section, to see what I mean.

Obviously, if we are using Show() & ShowDialog() then Hide() and Close() also need to be mentioned here. Most forms I create have a SELF:Close() in the event handler for the click of the "Close" push button.

One difference to VOers is that to enable and disable the form it is done as a property setting and not a method.

oMyForm:Enabled := TRUE

oMyForm:Enabled := FALSE

Above is the dotNet way to do it.

Before I go, I suggest you get used to designing your forms so that they may be resized and still look Okay - or, you must look out the tip in Section 29 to stop the user resizing.

Look at Section 22 on GUI controls - woops! we seem to have two Section 22's - good value for money !

Overall I found the event handling in Win Forms much easier to work with than VO windows. It seemed to encourage me to wish to do far more than I was used to delivering in my VO GUIs.

I have also enjoyed doing this new event "stuff" instead of being a bit terrified" as probably was the case in VO for more than the "bog standard" stuff.

PHH December 2007.


MessageBox

I thought is was time to give "air" to a sometimes over-looked tool. Just because the MessageBox class seems to be based on a very simple idea, does not mean it can't be used in many very useful ways.

I would suggest that any person coming to Vulcan.NET, and particularly from VO, gets to grips with the MessageBox "tool" as soon as possible.

Anyone using simple techniques to try and debug code in VO has probably used ? and inkey(n) to print out stuff to the black screen (console) in the past.

Well, now that this approach is no longer available, try missing out the console altogether and become a "Windows guy". Use the MessageBox class to put your messages on screen with an Okay button to close the dialog box once it is of no further use.

If you look at the Framework 2.0 SDK documentation you will see that the MessageBox class has plenty of overloads, meaning you can use a different number, and combination of parameters in the "calling" braces. You will need to experiment and give some of your time to getting a feel for this class. I can assure you it will be very worthwhile.

I have included some code from a current project I am developing to "Auto Update" an EXE file for a network application. Although the big app in question is now actually written in VO 2.8, I have decided to write the 'AutoUpdater' in Vulcan.NET (VDN).

You will notice, by examining the code below, that I would have ended up with a very long code line for the MessageBox if I had not bothered to tidy things up. This means it is easy to make mistakes.

By using the Static Methods and Enumerations in this way, we can make the code much more readable and easy to both copy, to other places required, and to maintain in the future.

Note the "SELF" in the parameters, simply means that the dialog box will appear on top of the object (window) specified. In this case the Auto Updater window.

There are four small image files attached to show the enumeration options for the various MessageBox related "sub-classes". The intellisense options are created from VIDE by Chris Pyrgas. This is a very useful feature and should be available in VS soon.

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

Please note that some of the icon enumeration options give actually the same icon as another. Why we couldn't have actually had a hand by now, beats me ! Call this progress ?

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

I have included an image from my own VO code to show what a hand actually looks like ;-)

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

Finally, remember the message box can be quite large if you fill it sufficiently :-) Look at Section 4 for Printer Settings !

I will put the code and related tips into Wiki, but without the images for now.

Note that the first line shown below is very useful to find out where the EXE file is so that I can look in the same place for the Config file. Dave Francis recently brought this up in a VOPS news group note on VO and it prompted me to find out how to do things in VDN. This is the first time since then that I have used it in "anger", as they say !!!

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

cStartPath := Application.StartupPath

Messagebox.Show(SELF,cStartPath,"Where the EXE started from !")

cDefaultPath := "C:\AutoUpdater3\"

cConfigFile := "configAU.PHH"

cFull := cDefaultPath+cConfigFile

IF Directory.Exists(cDefaultPath)

Messagebox.Show(SELF,"Yes, exists !","Directory Search")

ELSE

Messagebox.Show(SELF,"No, can't find it !!!","Directory Search")

Directory.CreateDirectory(cDefaultPath)

LOCAL oIcon AS MessageBoxIcon

oIcon := MessageBoxIcon.Hand

LOCAL oMBB AS MessageboxButtons

oMBB := MessageboxButtons.YesNo

LOCAL oDef AS MessageboxDefaultButton

oDef := MessageboxDefaultButton.Button1

LOCAL cMess, cHead AS STRING

cMess := "Make a new and blank/default settings Config. file ?"

cHead := "CREATE - New :"

LOCAL oDR AS DialogResult

oDR := Messagebox.Show(SELF,cMess, cHead, oMBB, oIcon, oDef)

IF oDR = DialogResult.Yes

SELF:CreateNewConfigFile()

ENDIF

ENDIF

Hope this helps improve your coding, readability, and applets generally.


DialogResult

The Button.DialogResult property and the Form.ShowDialog method use this enumeration.

You have seen in the last sub-section how the Messagebox could be setup so that a result could be accepted and used depending on the action of the user.

It is also possible to get a button to respond in a similar way. All you need to do is to have the button's DialogResult property set appropriately as a result of a click.

If you had three buttons on a form, Yes / No / Cancel - you could use their DialogResult to determine action to be taken.

You would need to make sure all three were previously NULL and then "poll" the buttons to see when to respond.

If you are interested in this latter use you will need to research it yourself. So far I have just used the Messagebox response to good effect.

PHH December 2007.


RichTextBox

The task of showing the contents of a RichTextBox control either at the top left, or bottom right of the displayed text, is shown below.

In my "Internal Reports" sub-module in ClickPrintVDN we have a push button for "Home" and "End" which are as follows :-

METHOD pbEndClick(o AS OBJECT , e AS System.EventArgs) AS VOID

//-------- go bottom right -----------

ortbPrintPreview:Select(ortbPrintPreview:TextLength-1,0)

ortbPrintPreview:Focus()

RETURN

METHOD pbHomeClick(o AS OBJECT , e AS System.EventArgs) AS VOID

//-------- go top left -----------

ortbPrintPreview:Select(0,0)

ortbPrintPreview:Focus()

RETURN

Obviously, there is a control called "rtbPrintPreview" and these methods, triggered by a push button click event do the required trick nicely.

(Thanks to Colin Newell.)

Don't forget that often when you are filling a Rich Text box with data, you have first processed quite a few DBF records, or similar.

I tend to make a sting of each line and put this along with a CRLF, into a growing StringBuilder object. This can speed up the process amazingly. I will show some examples later in this section.

PHH December 2007.


RadionButton

Useful events associated with Radio Button use are done on an individual control basis.

This is new to VOers where they have been used to 'groups' of buttons, and setting a group value.

Yes, we still have, and need, a group box in the toolbox pallet as it modifies the GUI behaviour of the buttons on the form within that box, so that two can't be 'checked' at once. However, the 'checking' or 'un-checking' of a RadioButton control triggers events at the individual control level.

From brief tests it would appear that at the form designer level, moving a RB out of the group box, or back into it, is all that is required to modify the behaviour of both it, and the group left in the box. Quite a joy to VOers of old :-)

You will notice that I user the word 'check' and not "press" for the RadioButton in Win Forms.

oMyRadioButton:Checked := TRUE

oMyRadioButton:Checked := TRUE

Although VOers will need to adjust their thinking a little, at least it is consistent with CheckBox controls :-)

Let us now look at a working example from some of my first demo code.

Below is an image of the app working. The selected column titles in the right hand list box can be manipulated depending on the event handler chosen for the list box.

Each of the radio buttons in the group box shown, will set a different event handler for the list box control.

First of all we will see how the individual radio button event 'Click' or 'CheckedChange' may be used to good effect.

How does this work ? When a RB is clicked the event is handled and the code executed sets a new event handler for the list box after clearing any already set.

In the Class statement the IDE (in this case VIDE) has put the following line of code.

PROTECT orbSortAZ AS System.Windows.Forms.RadioButton

Also in the InitializeForm() method the following lines appear :-

SELF:orbSortAZ := System.Windows.Forms.RadioButton{}

SELF:orbSortAZ:Name := "rbSortAZ"

SELF:orbSortAZ:Location := System.Drawing.Point{ 32 , 120 }

SELF:orbSortAZ:Size := System.Drawing.Size{ 104 , 24 }

SELF:orbSortAZ:TEXT := "Sort A-Z :"

SELF:orbSortAZ:Click += System.EventHandler{ SELF , @rbSortAZClick() }

SELF:orbSortAZ:TabIndex := 3

SELF:ogbModifyClickEffect:Controls:Add(SELF:orbSortAZ)

Of these, the one shown below is that which sets the event to be handled by the "rbSortAZClick" method if and when such a click takes place.

I have developed my own style over recent months, where I like to remove all such lines of code from the "InitializeForm" method into my own method called "SetMyEventHandlers()".

This is more flexible and gives the coder more control over situations which are more than simple.

You need to remember that more than one event handler may be set, so we also have to manage removing handlers in this situation, not for the RB's but for the list box control.

In my working code lines similar to that below are seen in the InitializeForm method for each RB in the group. These are fixed, and always cause the same method code to be run each time they (the individual RB) are clicked.

I have to say I prefer to use the "CheckedChange" event now, in place of the Click. But as long as it holds up under test then you choose what you want :-)

SELF:orbSortZA:Click += System.EventHandler{ SELF , @rbSortZAClick() }

Now, when the "SortAZ" RB is clicked the code below shows that the first action is to clear or remove all handlers set for the list box called 'selected'. This will be the same for each RB click. However, the next part which sets a new handler for the list box, is specific to the RB being clicked.

In this case it will set the method which sorts the contents of the list box from A to Z.

METHOD rbSortAZClick(o AS OBJECT , e AS System.EventArgs) AS VOID

oMyTT:SetToolTip(SELF:olbColsSelected, "Click to sort A-Z ....")

SELF:ClearAllHandlers4SelectedClick()

SELF:olbColsSelected:Click += System.EventHandler{ SELF,@ColumnSelectedSortAZClick() }

RETURN

Before we leave this example you may wish to know that the trick for clearing any handler set for the list box control, is shown in the code below :-

METHOD ClearAllHandlers4SelectedClick() AS VOID

SELF:olbColsSelected:Click -= System.EventHandler{ SELF,@ColumnSelectedRemoveClick() }

SELF:olbColsSelected:Click -= System.EventHandler{ SELF,@ColumnSelectedToTopClick() }

SELF:olbColsSelected:Click -= System.EventHandler{ SELF,@ColumnSelectedToBottomClick() }

SELF:olbColsSelected:Click -= System.EventHandler{ SELF,@ColumnSelectedSortAZClick() }

SELF:olbColsSelected:Click -= System.EventHandler{ SELF,@ColumnSelectedSortZAClick() }

SELF:olbColsSelected:Click -= System.EventHandler{ SELF,@ColumnSelectedClearClick() }

SELF:olbColsSelected:MouseDown -= MouseEventHandler{ SELF,@ColumnSelectedBothWaysClick() }

SELF:olbColsSelected:MouseDoubleClick -=MouseEventHandler{ SELF , @ColumnSelectedMouseDoubleClick() }

RETURN

Basically, I am removing each possible handler, and whether it is set, or not, does not seem to cause a problem at run time. We know that actually, only one or two of these ought to be set at any one time, but till I can find a way of finding out which are set, we will need to utilize this useful trick !

For more information on event handling look at Section 6.

A very positive bit of advice is - do not let the unfamiliarity of the code put you off, or make you think it is difficult - try it out on a small scale, get used to the syntax, and then - like me - realize it is quite easy to make interesting things happen. I now try to do much more interesting things with events than I ever did in VO. I am now different, it is just easier to manage once you get the knack.

PHH December 2007.


CheckBox

Check boxes are very useful in the GUI environment. And things just get better.

VOers coming to Win Forms will be used to these so there is not too much to say.

We will look at what is new, and how this can improve and enhance how we will do things.

After the new "stuff" we will concentrate on reacting to events of the CheckBox control - to see how we can usefully put these into operation on our Forms.

CheckState :

The Windows Forms check box control can have three different states. In order for this to be the case interactively, the ThreeState property has to be set to true.

If this property is set true then there is a related property 'CheckState' which has three different enumeration states after clicking. These are Checked, Unchecked, and Indeterminate.

Let us take this slowly so we get it right - it is very easy to get confused, as we can also set it to have just two states.

Lets take a screen shot of what three buttons look like in this situation.

When the form first shows all three check boxes look like the top one, with a blue centre. One click makes them go to what we are used to as unchecked, and a second click to the checked state.

On top of this behaviour it is possible to modify this so that when clicking, the box may only be in one of two states - the 'usual' behaviour - as we are used to from VO.

However, this all can add to the flexibility of how we use these as inputs. If we choose to have the check boxes initially displayed as Indeterminate, then there is a strong visual clue to the user as to whether they have set them to their choice - or just left the default value.

If the three state property is set to false then there is no way interactively the indeterminate state can be 'user set', but the code can detect it as remaining in that state. i.e. the user has not visited the check box, and it is as it was initially set.

If on the other hand we wish to capture a user input of type :- yes, no, and don't care / not bothered - then we set the ThreeState property to true. This means the mouse clicking can cycle the control through all the three states.

I could see that in the 'ThreeState false' situation, we could capture user preference of Yes / No and know they have selected. Say if we wish them to select a vegetarian meal. This way when our code looks to see the CheckState we know they have not yet made a selection if it is still in the initial Indeterminate state - and can guide their input accordingly.

Finally, if we don't initially display the control in indeterminate state, and the ThreeState is false, then the user never sees anything other than a two state control. Neither does the code for that matter :-)

Obviously, the checked state of the control is a static situation, and one which may be examined by the code at any time. However, if we wish a check box click, or selection, to trigger an action then we need to look at handling the appropriate event.

Let us try to code a suitable example to show all of these ideas in action. Although the example is a little forced it will do to show a few things relating to coding check boxes and reacting to a couple of their related events.

Let us see the form design for a simple airline flight option choice :-

Okay, what do we have here ? There are two check boxes which we will use to reset the inputs and another which will send the choices to the airline company.

These two controls will be set to be two state and have initial values of unchecked. So they will never have an indeterminate state. However, these two buttons will react to an event - the Reset Inputs to a 'click' event, and the Send Choices to a 'checked change' event.

On the other hand the three information collecting buttons will have three states (all of which may be selected by the user) and these will not be tied to any events at all.

Obviously, when the Reset control is clicked the three inputs are cleared and set to an initial sate of indeterminate. The Send control is forced to be unchecked and the Reset itself is returned to the unchecked state.

to be completed ........................

"ocbClickTest:ThreeState := TRUE"

SELF:ocbClickTest:CheckState := System.Windows.Forms.CheckState.Indeterminate

SELF:ocbClickTest:Text := "Click Test :"

PHH March 2008.


Full working code - GUI 1

Enter topic text here.


Section 2 - Data Structures

In this section on various forms of array and list, we need to take into account some history. We need to consider what type of array developers used in Visual Objects, and how these sit in the modern world of dotNet.

Because I found it confusing (to say the least) when I came to Vulcan in early days, I will try and explain things to avoid you needing to go through the agony too.

The array most VO developers loved and used a great deal was the data type called ARRAY. It used to be shown in red in the declaration as below :-

LOCAL aNames AS ARRAY //--------- VO arrays (dynamic) ---------

aNames := {}

These are not the arrays we will be studying in this section.

Because they come from the world of VO they may still be used in Vulcan, however, there is a new type of array in dotNet and this is the one which is in the System.Array namespace.

I would urge all users of Vulcan to go over to using nothing but the new form of array (and ArrayList) when writing new code.

For the sake of clarity we will sometimes refer to these dotNet arrays as array (SA), to show it is the class in the .NET System.Array namespace we are discussing.

Arrays (SA) can be made up of any object type and are declared using square braces as follows :-

LOCAL aSurnames AS STRING[] //------- the Type of Object is stated before the braces ------

It is possible to have multi-dimensional arrays declared as below :-

LOCAL aBothNames AS STRING[ , ]

However, in these days of objects, it seems pointless to keep doing things the old way.

I could see the point of possibly having a small, or simple, two dimensional array, say of first and surnames, but really, anything more complex is better handled as as object.

Using objects the problem of 'switching' indexes by mistake is removed altogether as properties are coded by name.

Please note I am telling you this as a recent convert from the VO style of coding. I used extensively, the dynamic ARRAY type in VO with the associated AAdd(), ASort() and AScan() - to great effect in business solutions.

I used to be quite proficient at coding suitable 'code blocks' to achieve what I required - even though I did not really understand 'how' they worked. The syntax made sense, but I did not have a clue what was happening under the covers.

After my development work on Vulcan I intend to move over to the .NET array and ArrayList class totally in my new coding. This is why I had to hassle people to provide me with some working code to copy and adapt, to allow me to sort and search arrays and arraylists. Well, Paul Piko came up with the goods, as usual - well done Paul. Now I can write code in Vulcan.NET to sort and search arrays and arraylists (and others) without knowing what is going on under the covers - strange old world ! Yes, I can understand the syntax and copy and adapt the ICompare classes I write, but don't really understand how they function.

Without being able to sort and search arrays and arraylist in an advanced fashion there is really not point in using them in the first place.

The bonus to us 'standard' sort of guys is that a lot of this knowledge is transferable, so it comes in handy with ListView controls too, and a few more I suspect as well.

Finally, to be totally honest, I have not yet figured out why I should not use only ArrayLists, as they are dynamic like those I used to use in VO. I find dynamic arrays very handy, and I don't mean resizing static arrays.

Finding Elements - Speed Issues :

In the following sub-sections of this section (Section 2) we see enough ways of sorting and searching to get you going, so you can get a foot into the dotNet world.

However, there are a few points to make generally which may help bring all this together.

If we have long lists, or arrays, we need to consider the time it may take to search through all the elements in order to locate a given item. If that is the case we need to choose which approach we use carefully.

Using the method BinarySearch() will be much quicker generally than using the static method Array.IndexOf(aTest, "find me")

If you are not bother about time issues then using the static method Array.IndexOf() is a good and easy way to see if a specified item is in your list. If found the returned value is a number within your bounds, otherwise a number one less than the lower bound. I suspect for must of us this means most of the time -1 will indicate a "no hit" situation.

Notice that there is also a static method Array.LastIndexOf() which does "just as it says on the can" ! It provides the index value for the last occurrence in the list.

If we realise that sorting the list prior to searching like this, means that all the like elements are "together", then we have an easy way to find, and extra if required, all the elements which are similar. So we could then go down the list, from first index to last index , and all the identical elements are accessed.

In section "ArrayList - Searching", towards the end, I show how we can use BinarySearch() and then some code to do what we describe above. The point of having a two different ways is down to time taken, the length of the list at the time, and what you as a developer at trying to do.

Since the list needs to be sorted for both approaches we can take that out of the equation. We also know that if we have sorted, we have a comparer which can be used to do a BinarySearch().

So if we have a long list then we locate our item by BianarySearch with just a handful of comparisons, where we need half the list number the other way - on average. For a lot of data in a list locating an element by BianarySearch() and the working from that element to find the first item (up the list) and the last item (down the list) is quick - compared to a n/2 search for both first and last occurrences. The difference may also be more obvious with different type of data - we need to test :-)

I am not sure whether it is easy to demonstrate this by timing the two methods, but we can try as below :-

First lets try to make a long test array and put in it numbers of type double (REAL8 is the same).

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

I am surprised by this result and will show you the code for you to see why below. First lets look for a different target number to see what that does to the times.

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

The first results above came from me trying the target as nTest+454545 and I can't explain the zeros except for suggesting that pointers somewhere keep their value and another search is not done in the same way. From testing here the binary search always gives zero for the second go, and rarely the IndexOf().

So for simple data types it would appear that IndexOf() is okay for general use, as LastIndexOf().

However, I have done another test with Decimal types and the results are very different indeed.

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

First thing I noticed was that the sort part of our test took an age compared to when using REAL8 (or type Double). So the penalty for Decimal type use starts to be apparent - only use it when you really have to for monetary calculations.

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

This looks to be almost two orders of magnitude slower than the results below, 8000 as compared to 90.

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

I think I will leave this discussion where it is at the moment and say that you need to use the code I will place in the full working sub-section, and for you to try things for yourself. You will gain experience with coding and syntax, at the same time as getting a feel for ways of sorting and searching with various data types.

Good Luck !

PHH Late 2007 - Mar 2008.


Arrays

Where are we headed ?

Just a note before we get going on lots of code detail and live testing.

It is easy in dotNet to fill form control like combo boxes and list boxes with dotNet standard arrays, but the ArrayList class is a great swap for the dynamic VO array we all love and need to move away from.

So don't forget to look up the sub-section on making arrays from arraylists. This means we can have the best of both worlds. We can have our cake, and eat it.

At the end of this sub-section we will see how to start doing such useful things, don't forget to look into this useful approach.

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

Arrays (SA) can be made up of any object type and are declared using square braces as follows :-

LOCAL aFullNames AS STRING[] //------- the Type of Object is stated before the braces ------

LOCAL aAges AS INT[]

LOCAL aSalaries AS FLOAT[]

LOCAL aDoBs AS DateTime[] //---------- see Section 18 to find out why we will not be using DATE & Time -----------

We will look at some other varieties later.

Obviously, if the array (SA) is declared as above then it has to be strongly typed.

This form of array needs to be set to a fixed length when it is instantiated, but this may be changed at a later stage of processing using the method Resize(). We will discuss this later.

LOCAL aNames AS STRING[]

Let us use the names array to show how we may instantiate our arrays (SA) in our code :-

Alternatively, instead of using a 'number' fixed at coding time, we can use a variable which will use the value it has at run-time to set the length of the array (SA).

The syntax for this is as follows :-

However, the value required may be totally unknown until the time of instantiation. Let us look at a good 'real life' example, where we require the list of printers installed on the PC being used to run the application.

Look at the working sample code below and we will discuss it a little further afterwards.

It is only possible to know the number of installed Windows printers at the time the code is run. This class of PrinterSettings.StringCollection makes it very easy for the developer to access the list of printer names and you will see it used again to good effect in Section 4 - Printers.

It is possible to use the CopyTo() method of this class to place the printer names in an empty array (SA) as shown directly below :-

Note the index for the position of the "CopyTo" process is zero. Although our Vulcan default arrays are set to be one based, when we use a dotNet method call we need to use zero as the system uses zero for its arrays all the time. To find out more about this zero and one base situation read Don Caton's article in Wiki at 'GoVulcan.net'.

To translate - it means copy the printers list from "the system" into the user defined array called aPrinters by starting at the first element.

Now that the names of the printers are in our array we may do with them as we choose, like fill a combo box control, to allow user interaction.

See the section on printers (Section 4) to see this in action - or the section on GUI form controls(II) - which is Section 22.

To have a change from an array (SA) of strings, let us look at making an array of DateTime objects. We will fill a short array of length three with such objects and try to print out some detail of the third element.

Remember, this sample working code has been done in VIDE with the default compiler switch such that user defined arrays have an index of one.

Images too may be put into an array (SA) so let us see what the syntax looks like and study some working code.

Now we can have a brief look at something potentially very useful, meaning that it may well be required on a regular basis in many applications.

The FileInfo class is dealt with in detail in section 16 on "Drives, Folders, and Files" - a strongly typed array may also be made of FileInfo objects as shown below :-

Don't forget there are two other important objects you may wish to include in arrays of your own, and these are boolean and characters.

The syntax looks like this :-

Finally, let us consider a user defined or "custom" class, and then make an array of these.

The class we will use is the PatientRecord class which is used thoroughly in sub-section on ArrayLists, which is later in this section.

The PatientRecord class is as shown below :-

Below we will just see how to declare and create a short array of type PatientRecord, we will use such arrays much more when we come to sorting and searching in the next sub-section.

As an exercise for you to do, look up the FileInfo class in the Framework 2.0 SDK documentation and make a working test app which uses the System namespace and declares a strongly typed array (SA) of DriveInfo objects. Try to fill these from the drives on the PC which you are using.

We will look in more detail at how useful the DriveInfo class is in section 16 on "Drives, Folders and Files".

We will see how to resize arrays (SA) in a later sub-section. Meanwhile, if you wish to have dynamic arrays then choose the ArrayList class - see the sub-section to follow shortly.

Making Arrays from ArrayLists :-

First of all lets start with a simple arraylist of names and convert this into an array which could be used to fill a combo box subsequently.

An array of a suitable type needs to exist before we copy in the details we require. See how this looks below :-

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

So from section 22 we can borrow a line of code :-

ocbDataTypes:items:AddRange(aTypes)

If we supplied our new array as parameter, then the combo box would be filled with names and not data types :-

ocbDataTypes:items:AddRange(aNames)

In fact if we had a few check boxes or radio buttons we could set up an interactive window where one combo box could have a while set of different contents depending on the check box or radio button in focus. Obviously we would need to have suitable event handlers coded and switched, or set, on.

The SDK has this to say :-

Copies the entire ArrayList to a compatible one-dimensional Array, starting at the beginning of the target array.

There is another way to make an array using the ToArray() method of the arraylist class.

Copies the elements of the ArrayList to a new array.

You will need to explore further the options with these different methods if you wish to use more complex objects as the list elements.

This short note will at least point you in the right direction, and alert you to the fact that such conversions can be easily made, once you know what may be done.

I for one will tend to 'work' with arraylist objects and the convert to arrays for such things as filling form controls. We all have to force ourselves to move away from VO dynamic arrays if we wish to get the best out of the dotNet world.

PHH 2007 - March 2008.


Arrays - Sorting & Searching

Before we see how to sort an Array (SA) let us make one from a different type of object. Let us use DateTime objects, and then TimeSpan ones, just to confirm that arrays may be of any object type.

For simplicity, we will start with a small array of three DateTime 'entities' where just the date portion has been specified.

When we sort them into order 'first to last' we would expect the first date to be 1st. Jan. 2006 - which indeed is what is output in the MessageBox.

Note that the sort that is being used is the one which is "built-in" so to speak and if we wish to provide a more complex or comprehensive sort then we may do so. This should be particularly useful for objects which are more than simple in their form and structure. We will see have to sort an array (SA) of our custom object 'PatientRecord' later in this section, where we lay-out a variety of ways to sort our objects. If we can manage this, then we ought to be able to sort anything thereafter.

For now, let us return to the DateTime array and then supply data which is time related rather than the above date related values.

There is another very useful static method which we may use in conjunction with the sort method, it is the 'reverse' method.

If we combine this with the use of the sort static method then we can make our order 'last to first'.

Lets try this out now to see if we get the desired result of four minutes past four o'clock as our first revere element :-

This would appear to work just as we would wish it to, and so now we can sort into both 'first to last' and 'last to first' orders.

Let us see this simple sort in action on other types of object we have in our arrays (SA). First some numbers of type LONG.

This seems to do as we would wish as -45 is the lowest long integer in the list of seven and 302 the highest.

However, when I try to use numbers of type FLOAT I seem to always get a runtime error. You may wish to try this for yourself in case it is just due at my end to me using a pre-release version of the compiler. The 'Reverse' static method seems to work OK but not the sort.

Let us try a sure bet, and put some strings into reverse order, that will be Z to A.

Right, now that the easy stuff is out of the way, here is a more advanced approach to sorting any objects you have, either system or customized, and as a bonus, the method of approach works for ArrayLists and GUI controls like ListView. Once we get our head round it we can apply it in many places with success.

For those of you who are coming fro the world of Visual Objects then it will be a little bit like learning how to modify and apply code blocks to sort arrays in VO. I could do quite a bit, easily, and with success. But I would never say I thought I understood what was going on under the hood !

Look at the code and output from the example below, yes, we have written our own class to sort the float array (SA) into Biggest to Smallest order. And indeed it appears to gibe us what we require.

Notice the static sort method now is supplied with another object along with the array to be sorted. This object is an instantiation of the class 'MyFloatBS' which we will see shown below. First let us examine the code and the output from the sorting.

Now that we see that this approach works, let us examine the code for the class 'MyFloatBS' and see just how straight forward it actually is.

As long as we learn how to adapt the code in this class and how to call on it when required, we will be able to sort just about everything we wish.

Please note that I am doing as I preach, as I was first given the code for this approach from Paul Piko for sorting ArrayLists, and have just learned how to apply it when necessary. You may wish to look at the ArrayList sub-section in more detail as we cover in that section, how to have a multi-option sort which is useful for complex object classes, say like the custom PatientRecord class.

For now, sorting numbers of type FLOAT, there is only really one way we wish to sort them. So we will use this single use class.

Note I could have just made a straight forward sort for small to large, and the used the static method 'reverse' to get the list of items the other way round.

Let us now try to apply this new found approach to sorting some different objects. Let us make an array (SA) of FileInfo objects and try and sort them into A-Z alphabetical order by their filename. When we succeed with that we will try to sort them by file size, and then by the date of last modification.

Writing this last couple of sentences I realized that we are sounding as if we are starting to do something which is of worth.

If you are unfamiliar with the FileInfo class then first check out section 16 on 'Drives, Folders, and Files'. This has worked examples of the FileInfo class in action. Notice that we will need to use namespace "System.IO".

Because this approach is so general, we can attempt to write the IComparer sort class first, before we even think about making our array (SA). All we need to know are the names of the properties we will need to use belonging to the FileInfo class.

We will use 'Name' which is a STRING, 'Length' which is of type LONG, and 'LastWriteDate' which is DateTime object.

Let us modify the above class to make a new one called 'MyFilesAZ' and we should get something as shown below :-

All we need to do is make sure we 'cast' our objects supplied as parameter inputs, to the type we know we are using, in this case 'FileInfo' then we have access to the properties we require.

While we are at it, we may as well write the other two IComparer classes as well, so lets have ago at the file size - small to large.

I timed myself doing this in the VIDE editor using copy and paste, with just five edits of the text it took about 30 seconds. And don't forget this is all new to me too! Let use see what we have.

In this third and final class I have had to use the objects label 1 & 2 in the other order so that the most recent (big date and time) is before the oldest (small date and time).

All we need now is an array (SA) to test these on so lets get to it - see below :-

You will see that although we got our files with names in A-Z order, we have the full path. This is because I have coded in such a way as to print out the FileInfo object 'ToString' and not the 'Name' property as I intended. Still, this will be a good opportunity for you to try this out for yourself, and produce a list without the equivalent of "C:\VDNUK\" on the front of each filename.

Obviously, you will need to choose files which exist on your PC in order to get the code to function correctly.

If we concentrate on the line of coding which uses the static method 'sort' of the System.Array class, it is not difficult to see that to get a different sort order we only need to change the name of the IComparer object supplied as a parameter.

However, it helps to change the message box heading to reflect our different output, and to also print the correct details. No more of my mistakes as in the example above !!

Finally, lets change the same few lines in order to get the list in order of when last modified.

Without much thought it is easy to guess which files will have exactly the same data and time when last modified, I would guess the DBF and CDX files called "PAT32".

We seem to have come a long way and now have some useful ideas to aid our application programming. There is one more step we could take if pushed, and that is to have all our sorts rolled together for one type of array object we wish to sort. All these three could be put together and called "AllSortsFileInfo" as an ICompare class. To see how this is achieved look at the section on ArrayLists. It is also a useful example of enumeration in action.

I hope you are on your way now with sorting all types of array (SA) and don't forget to have some fun in learning how to put these ideas into practise.

When you get good with sorting, go further and study how to search arrays (SA) for elements we wish to find.

Searching :

Are you ready for the biggest surprise of your life ?

Well, it is that now we have the IComparer for the FLOAT array (SA) we can very simply use this in a single line code call to the static method 'BinarySearch' and we have a working search which can be seen below.

The SDK documentation tells us to sort the array first then call the BinarySearch method with three parameters. The code below shows the first is the sorted array (SA), the second the object which you are searching for, and thirdly the IComparer object required to do the job. Examine the code below for these changes :-

The static method 'BinarySearch' returns the index value of the position in the array where the target object is found. Here in our example the target object was the float with value 5.0 and the index value returned was actually just "2".

However, you need to remember that the System.Array class static method would be dealing in zero based arrays and so we need to add an extra one if our Vulcan default is set to 'one' based, as mine is. I must admit I am not having too much trouble with the mixed array bases, so don't worry - just get going and have a go - you will soon get the hang of it !

Anyway, it gives you a real purpose to test your code thoroughly if you are not sure about your "0"s & "1"s.

So to summarise, if you create a suitable IComparer class for your stored objects then you may do array sorting as well as searching - what a bonus ?!

Now you know enough to go off and do your own searching.

Find All :

Oh! before you do, a final important point to make. The point of sorting before searching, the binary search method needs the elements to be sorted in order to guarantee a 'find'.

The binary search will find an occurrence of a target item in a list, if it is somewhere in the list. However, it will not necessarily be the first occurrence. It could be the last, and most likely for multiple occurrences, one of the ones in between. So if you need them all you can easily extend your search as follows :-

From your first "hit", go backwards in the sorted list (towards the top) until the elements is not equal to the target value.

Once you have located this position, the first item in the list, continue to go down the array until the item is different to your target item. You may wish to store the index values at the same time. Maybe in an array called 'AllHits', although the index range is sufficient for most cases.

Where you may wish to change your approach, and extract actual elements, is if the elements are complex objects - say customers. And you have located by the comparer method you provided, a way of locating surnames. OK all the "Jones" you have found - BUT - the rest of their information will be different, so you may wish to extract the customer objects for further processing.

Check out the Array.Copy() static method as this allows you to specify the starting index and the length of list to copy over to the specified target array.

The options are many and various, you just have to adapt to fit the problem in focus. Anyway try extracting the similar elements from an example like that above.

This will make a useful extension exercise for you to do to enjoy your new-found skills in array (SA) sorting and searching.

For even more information on sorting and searching check out the end of the main page for this Section 2 on Data Structures.

Good Luck.

PHH late 2007 - March 2008.


ArrayList- class

Namespace : - System.Collections

#using System.Collections

Vulcan and .NET has a range of types of Array which are available to the developer.

The ArrayList is very flexible and may be most familiar to the persons moving fron the world of Visual Objects.

The example shown below uses a database server as the source of the patient information, and reads in over thirteen hundred records.

The array has a zero based index, and can have any number of elements added to it.

You will also notice when studying the code that the large, or main arraylist, stores lots of shorter arraylists.

These short array lists hold each patient's data.

Will will see in another Wiki article that we may have an arraylist holding objects which hold the patient details.

For those from the world of VO, it will also hold the VO Array type.

In fact an ArrayList simply stores objects.

At the moment we need to "cast" the object retrieved from the arraylist so that it has the form required.

This is what :-

"aListSh := (ArrayList)aList[13-1]"

is doing in the code.

You will see that we do a similar thing when we retrieve the customised object stored in the following article mentioned above.

METHOD MakeShowArrayList() AS VOID

LOCAL nCount AS LONG

LOCAL cName, cFirstnames, cSurname, cText AS STRING

LOCAL lActive AS LOGIC

LOCAL nPatRef AS LONG

LOCAL aList, aListSh AS ArrayList

aList := ArrayList{}

oServerPA:GoTop()

nCount := 0

DO WHILE ! oServerPA:EOF

cSurname := ALLTRIM(oServerPA:fieldget("surname"))

cFirstnames := ALLTRIM(oServerPA:fieldget("firstnames"))

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

cName := cFirstnames+SPACE(1)+cSurname

nPatRef := oServerPA:fieldget("patunref")

lActive := oServerPA:fieldget("patactive")

aListSh := ArrayList{}

aListSh:Add(cName)

aListSh:Add(lActive)

aListSh:Add(nPatRef)

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

aList:Add(aListSh)

nCount +=1

IF nCount > 1313

EXIT

ENDIF

oServerPA:Skip(+1)

ENDDO

aListSh := (ArrayList)aList[13-1]

cText := "Number of elements :-"+NTrim(aList:Count)+CRLF+CRLF

cText += "Patient No. 13 :"+CRLF

cText += "-----------------------------------"+CRLF

cText += aListSh[0]:ToString()+CRLF

cText += aListSh[2]:ToString()+CRLF

MessageBox.Show(SELF,cText,"Patient Details : from ArrayList :")

RETURN

The code above should be enough to get you going using ArrayList in your own code.

It is well worth a visit to the Framework 2.0 SDK documentation to see what is has to offer on the ArrayList class in .NET.

Set up a test example and try out some more of the methods, like Clear, CopyTo and AddRange.

Don't forget to see the sub-section (below) on storing customized objects in an ArrayList.

PHH Oct.2007

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

Using Customized Objects :-

There is an article in Wiki on using an ArrayList to store shorter ArrayLists, which hold individual patient records.

Here we will look at how the main (or long) ArrayList can store a 'list' of your objects.

These objects are of a customized class called "PatientRecord" and the code for this class is shown below.

CLASS PatientRecord

EXPORT Surname, Firstnames, Fullname AS STRING

EXPORT DoB AS DATE

EXPORT Age, PatRef AS LONG

EXPORT Title AS STRING

EXPORT LastPayment AS FLOAT

CONSTRUCTOR()

SUPER()

SELF:Fullname := ""

SELF:Surname := ""

SELF:Firstnames := ""

SELF:PatRef := 0

SELF:Dob := CTOD("--/--/----")

SELF:Age := 0

SELF:Title := ""

SELF:LastPayment := 0.0

RETURN

END CLASS

We now need to see how this class can be used in conjuntion with the main ArrayList.

The code below is from a working demo example of data being read from a data file of DBFCDX format and stored for use later.

There are thirteen hundred records being stored, and so we have thirteen hundred objects in the ArrayList.

However, it only takes about a second for the message box to show with the retireved details of the 13th. element.

METHOD MakeShowArrayList2() AS VOID

LOCAL nCount AS LONG

LOCAL cName, cFirstnames, cSurname, cText, cTitle AS STRING

LOCAL lActive AS LOGIC

LOCAL nPatRef AS LONG

LOCAL aList AS ArrayList

LOCAL oPatRec AS PatientRecord

aList := ArrayList{}

oServerPA:GoTop()

nCount := 0

DO WHILE ! oServerPA:EOF

cSurname := ALLTRIM(oServerPA:fieldget("surname"))

cFirstnames := ALLTRIM(oServerPA:fieldget("firstnames"))

cTitle := ALLTRIM(oServerPA:fieldget("title"))

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

cName := cFirstnames+SPACE(1)+cSurname

nPatRef := oServerPA:fieldget("patunref")

lActive := oServerPA:fieldget("patactive")

oPatRec := PatientRecord{}

oPatRec:Fullname := cName

oPatRec:Title := cTitle

oPatRec:Surname := cSurname

oPatRec:Firstnames := cFirstnames

oPatRec:PatRef := nPatRef

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

aList:Add(oPatRec)

nCount +=1

IF nCount > 1313

EXIT

ENDIF

oServerPA:Skip(+1)

ENDDO

oPatRec := (PatientRecord)aList[13-1]

cText := "Number of elements :-"+NTrim(aList:Count)+CRLF+CRLF

cText += "Patient No. 13 : [ customized object ]"+CRLF

cText += "--------------------------------------"+CRLF

cText += oPatRec:Fullname+CRLF

cText += oPatRec:PatRef:ToString()+CRLF

MessageBox.Show(SELF,cText,"Patient Details : from ArrayList :")

RETURN

Don't forget to consult the Framework 2.0 SDK documentation on the ArrayList class to see what other useful properties and methods may be lurking there for you to find and use.

PHH Oct.2007

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

Starting to Sort :-

Now that the earlier articles on the ArrayList class in Wiki have shown how to store, and also gain access to objects in arraylists, we will now focus our attention on sorting these lists of items into different set orders.

The approach used below was supplied to me by Paul Piko, and I am very grateful indeed. I mention more about Paul and his teaching materials at the end of this article.

Lists are of most use when they are put into a specific order, so that they can then be applied for a definite purpose.

The simplest example is probably puting a list of names into alphabetic order. For a mailing list for example.

If our arraylist had simply got surnames as each item, then using the code :-

aList:Sort()

would put the list into such an order, A-Z.

The example in the Framework 2.0 SDK documentation puts the ArrayList "The, quick, brown, fox ......" into order :- "brown, dog, fox, ......"

However, we are rarely going to have such a simple list of string objects. We are much more likely to have a list of customized objects like the "PatientRecord" in the earlier examples in Wiki I mentioned above.

Lets look at a typical such "PatientRecord" class :-

CLASS PatientRecord

EXPORT Surname, Firstnames, Fullname AS STRING

EXPORT DoB, LastOrderDate AS DATE

EXPORT Age, PatRef AS LONG

EXPORT Title AS STRING

EXPORT LastPayment AS FLOAT

EXPORT LastOrderNum AS LONG

CONSTRUCTOR()

SUPER()

LOCAL dBlank AS DATE

dBlank := CTOD("--/--/----")

SELF:Fullname := ""

SELF:Surname := ""

SELF:Firstnames := ""

SELF:PatRef := 0

SELF:Dob := dBlank

SELF:LastOrderDate := dBlank

SELF:Age := 0

SELF:Title := ""

SELF:LastPayment := 0.0

SELF:LastOrderNum := 0

RETURN

Let us assume that we want to put the list into order of "surname + firstnames" such that "Jones Anne" comes before "Jones Mary", and that "Jones" is above "Williams" in the list. Although this is not much more than our simple surname order, we can change the supplied code below, only slightly, to do many very different and more complex orders.

One approach which is very flexible, as it can easily be adapted to sort in a varietiy of ways, is to write a new class, in possibly a way that is quite new to you. Let us call the class "PatientAscendSort" and this will be used in the following way, to sort the list :-

aList:Sort( PatientAscendSort{} )

Now let us see what Paul's new class looks like :-

CLASS PatientAscendSort IMPLEMENTS IComparer

VIRTUAL METHOD Compare(o1 AS OBJECT, o2 AS OBJECT) AS INT

LOCAL oPatRec1, oPatRec2 AS PatientRecord

oPatRec1 := (PatientRecord)o1

oPatRec2 := (PatientRecord)o2

// Compare first with second for ascending sort

// Compare second with first to get descending sort

LOCAL result AS INT

result := STRING.Compare( oPatRec1:Surname+oPatRec1:Firstnames, oPatRec2:Surname+oPatRec2:Firstnames )

RETURN result

END CLASS

Note: The use of surname + firstnames is simple for this example but may produce undesirable results when surnames of different lengths occur. You could pad the surname portion of this expression for a better result.

Please note that Paul Piko supplied the details of this method of sorting, I only made some small adaptations afterwards. I will also copy and paste this approach in many more places to achieve the list orders I require in the future.

This same method may be used on other .NET list types other than the ArrayList class, so it really is worth learning.

It is highly recommended that you get hold of any of Paul's publications / notes and study them thoroughly.

I have picked up more than just a few gems, and used them to provide some of the Wiki and "Pearls" articles, as well as a number of sections of these notes.

These are "Vulcan.NET At Warp Speed" - available from:- the official Vulcan site: http://www.govulcan.net

Also Paul's information site, Vulcan Mind Meld:- http://www.vulcanmindmeld.net The latest version is October 2007.

In order to study ways of adapting this approach to produce different sorted orders see the next article in Wiki - "ArrayList : Sorting (Two)" which is in fact the next sub-section of the notes.

PHH Oct.2007


ArrayList - more Sorting

In the first article in Wiki on sorting arraylists, we saw how by writing a new class, we could then apply this in the sort method to achieve the set order we required.

Let us now start to change and adapt the code to produce different list orders.

First let us make a simple change and sort Z-A, or make a descending sort. The code shown below does this :-

CLASS PatientDescendSort IMPLEMENTS IComparer

VIRTUAL METHOD Compare(o1 AS OBJECT, o2 AS OBJECT) AS INT

LOCAL oPatRec1, oPatRec2 AS PatientRecord

oPatRec1 := (PatientRecord)o1

oPatRec2 := (PatientRecord)o2

// Would normally compare first with second for ascending sort

// Here we compare second with first to get descending sort

LOCAL result AS INT

result := STRING.Compare( oPatRec2:Surname+oPatRec2:Firstnames, oPatRec1:Surname+oPatRec1:Firstnames )

RETURN result

END CLASS

Here we have simply reversed the order of the items being compared, in order to produce our descending sort.

In the next example we take things a lot further, and we will start to see how we can just about do anything we like on the sorting front !

Let us set ourselves the target of printing out the last twenty patients to make an order with our company.

Since the order number for transactions is sequential, and unique, we may use the "LastOrderNumber" property in the "PatientRecord" class to help us get what we are after.

If we write a similar new class to that above and call it "PatientMostRecentCustomer" we will be well on our way to getting our desired list.

Note we have replaced the "STRING.Compare()" with a CASE structure which is the generalised form of the comparison needed for sorting. In fact if the "STRING.Compare()" had not existed we could have written our own replacment with a CASE structure similar to the one shown below.

Because we wish to have the patient with the highest order number at the top of our list (effectively Z-A) we compare second item with first in our code. You may need to play around and do live tests to get a feel for how this works. Then you will be off, and doing your own stuff !

CLASS PatientMostRecentCustomer IMPLEMENTS IComparer

VIRTUAL METHOD Compare(o1 AS OBJECT, o2 AS OBJECT) AS INT

LOCAL oPatRec1, oPatRec2 AS PatientRecord

oPatRec1 := (PatientRecord)o1

oPatRec2 := (PatientRecord)o2

// Would normally compare first with second for ascending sort

LOCAL result AS INT

DO CASE

CASE oPatRec2:LastOrderNum < oPatRec1:LastOrderNum

result := -1

CASE oPatRec2:LastOrderNum > oPatRec1:LastOrderNum

result := +1

OTHERWISE

result := 0

END CASE

RETURN result

END CLASS

To apply this sort just change the code line to :-

aList:Sort( PatientMostRecentCustomer{} )

With our arraylist now in order of highest order number first, we can print out from the first twenty items in the newly ordered list.

Obviously, the item with index zero holds the latest patient customer details.

LOCAL i AS LONG

cText := ""

FOR i := 0 UPTO 19

oPatRec := (PatientRecord)aList[i]

cText += oPatRec:Surname+oPatRec:LastOrderNum:ToString()+CRLF

NEXT // i

MessageBox.Show(SELF,cText,"Patient Details : from ArrayList : Latest Customers :")

This appears to work fine for me in my tests. Although the message box display is showing a fixed length for the surname field, the proportionally spaced font makes the order number output "wavey" !

Also, in case an eagle eyed Paul reads this :-) I realise that what we have done is not actually what we set out to do, if we are being "picky". The list we have produced is fine if none of the listed patients has very recently made two orders in rapid succession.

Still, considering that a day or two ago I could not sort customised complex records at all, I will settle for what we have done here.

PHH Oct.2007

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

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

All Sorts of Sorts !

Now that we have seen in earlier articles on the ArrayList class, how to store and sort complex customized objects, lets us look at a tidy way of having a number of different sorts.

It would be nice to have at our finger tips, ready to use in our code, a few different sort orders which could be simply chosen when we need them.

If we look at the code below we can see that enumeration can be used to easily and clearly help us select which type of sort we require.

I have commented lines which would produce other sort orders, to help us see what can be done.

You will notice that a parameter is supplied to the "aList:Sort()" code line. So that instead of getting the default sort order, or the one supplied as a 'virtual method' with the "PatientRecord" class (see earlier article in Wiki), we get an order selected by the enumeration.

  LOCAL nMyType AS DWORD

//nMyType := PatientRecordSortType.SurnameFirstnamesAZ

//nMyType := PatientRecordSortType.SurnameFirstnamesZA

//nMyType := PatientRecordSortType.MostRecentCustomer

//nMyType := PatientRecordSortType.PostCodeAscend

nMyType := PatientRecordSortType.PatientUniqueReference

aList:Sort(PatientRecordAllSorts{nMyType})

The parameter is "PatientRecordAllSorts{nMyType}" and instantiates a class called "PatientRecordAllSorts", which is basically a group of methods doing different sorts.

>> Note the enumeration can be simplified (Chris Pygas & Karl Faller) to not include the MEMBER syntax. <<

ENUM PatientRecordSortType

SurnameFirstnamesAZ := 1

SurnameFirstnamesZA := 2

MostRecentCustomer := 3

PatientUniqueReference := 4

SurnameAZ := 5

SurnameZA := 6

LongestWithCompany := 7

PostCodeAscend := 8

END ENUM

We now need to see just what this new class called "PatientRecordAllSorts" looks like.

CLASS PatientRecordAllSorts IMPLEMENTS IComparer

PROTECT WhichSort AS DWORD

CONSTRUCTOR(nSortType AS DWORD)

SUPER()

SELF:WhichSort := nSortType

RETURN

VIRTUAL METHOD Compare(o1 AS OBJECT, o2 AS OBJECT) AS INT

LOCAL oPatRec1, oPatRec2 AS PatientRecord

oPatRec1 := (PatientRecord)o1

oPatRec2 := (PatientRecord)o2

// Would normally compare first with second for ascending sort

// SurnameFirstnamesAZ >> 1

// SurnameFirstnamesZA >> 2

// MostRecentCustomer >> 3

// PatientUniqueReference >> 4

// SurnameAZ >> 5

// SurnameZA >> 6

// LongestWithCompany >> 7

// PostCode >> 8

LOCAL result AS INT

DO CASE

CASE SELF:WhichSort = 1

result := SELF:SurFirstAZ(oPatRec1, oPatRec2)

CASE SELF:WhichSort = 2

result := SELF:SurFirstZA(oPatRec1, oPatRec2)

CASE SELF:WhichSort = 3

result := SELF:MostRecentCust(oPatRec1, oPatRec2)

CASE SELF:WhichSort = 4

result := SELF:PatUniqueRef(oPatRec1, oPatRec2)

CASE SELF:WhichSort = 5

result := SELF:SurAZ(oPatRec1, oPatRec2)

CASE SELF:WhichSort = 6

result := SELF:SurZA(oPatRec1, oPatRec2)

CASE SELF:WhichSort = 8

result := SELF:PostCodeAscend(oPatRec1, oPatRec2)

OTHERWISE

END CASE

RETURN result

METHOD SurFirstAZ(oPatRec1 AS PatientRecord, oPatRec2 AS PatientRecord) AS INT

LOCAL result AS INT

result := STRING.Compare( oPatRec1:Surname+oPatRec1:Firstnames, oPatRec2:Surname+oPatRec2:Firstnames )

RETURN result

METHOD SurFirstZA(oPatRec1 AS PatientRecord, oPatRec2 AS PatientRecord) AS INT

LOCAL result AS INT

result := STRING.Compare( oPatRec2:Surname+oPatRec2:Firstnames, oPatRec1:Surname+oPatRec1:Firstnames )

RETURN result

METHOD MostRecentCust(oPatRec1 AS PatientRecord, oPatRec2 AS PatientRecord) AS INT

LOCAL result AS INT

DO CASE

CASE oPatRec2:LastOrderNum < oPatRec1:LastOrderNum

result := -1

CASE oPatRec2:LastOrderNum > oPatRec1:LastOrderNum

result := +1

OTHERWISE

result := 0

END CASE

RETURN result

METHOD PatUniqueRef(oPatRec1 AS PatientRecord, oPatRec2 AS PatientRecord) AS INT

LOCAL result AS INT

DO CASE

CASE oPatRec1:PatRef < oPatRec2:PatRef

result := -1

CASE oPatRec1:PatRef > oPatRec2:PatRef

result := +1

OTHERWISE

result := 0

END CASE

RETURN result

METHOD SurAZ(oPatRec1 AS PatientRecord, oPatRec2 AS PatientRecord) AS INT

LOCAL result AS INT

result := STRING.Compare( oPatRec1:Surname, oPatRec2:Surname )

RETURN result

METHOD SurZA(oPatRec1 AS PatientRecord, oPatRec2 AS PatientRecord) AS INT

LOCAL result AS INT

result := STRING.Compare( oPatRec2:Surname, oPatRec1:Surname )

RETURN result

METHOD PostCodeAscend(oPatRec1 AS PatientRecord, oPatRec2 AS PatientRecord) AS INT

LOCAL result AS INT

result := STRING.Compare( oPatRec1:PostCode, oPatRec2:PostCode )

RETURN result

END CLASS

Although it is longer than the usual amount of code I like to display in Wiki, it does show how simple and straight forward the approach is, to having a number of sorts rolled into one class.

You will notice that there is one sort yet to be included, and that is number seven, the order where the patients which have been ordering with the company the longest, are at the top. It is missing simply because I have a few issues with DBF stored dates with version 142 of Vulcan I am using.

I will add this later when I get to grips with my date problems.

Please note that you could combine this "All Sorts" approach with that of the "Main" sort approach in earlier articles, and have the best of both worlds.

PHH Oct.2007 >>Thanks go to Paul Piko for the original sort code samples.<<

A late night contribution from Paul as he prepares for Germany and conference time.

He suggested changing the code as follows, so getting the correct number etc. does not play a part ! "Well spotted Batman"

LOCAL result AS INT

DO CASE

CASE SELF:WhichSort = PatientRecordSortType.SurnameFirstnamesAZ

result := SELF:SurFirstAZ(oPatRec1, oPatRec2)

CASE SELF:WhichSort = PatientRecordSortType.SurnameFirstnamesZA

result := SELF:SurFirstZA(oPatRec1, oPatRec2)

CASE SELF:WhichSort = PatientRecordSortType.MostRecentCustomer

result := SELF:MostRecentCust(oPatRec1, oPatRec2)

CASE SELF:WhichSort = PatientRecordSortType.PatientUniqueReference

result := SELF:PatUniqueRef(oPatRec1, oPatRec2)

CASE SELF:WhichSort = PatientRecordSortType.SurnameAZ

result := SELF:SurAZ(oPatRec1, oPatRec2)

CASE SELF:WhichSort = PatientRecordSortType.SurnameZA

result := SELF:SurZA(oPatRec1, oPatRec2)

CASE SELF:WhichSort = PatientRecordSortType.PostCode

result := SELF:PostCodeAscend(oPatRec1, oPatRec2)

OTHERWISE

END CASE

PHH with PP's ideas :-)
=================================================================

Sorry folks,

I was so busy try to get the detail correct and not misleading, that I forgot to say thank you to Paul Piko for the original code to do things in this way. (And the other way too!)

I strongly recommend that all folk wishing to use Vulcan read his book and look out for any other sets of notes he may offer to the community.

I have learned some very useful things by working through Paul's notes, even when I thought the content was "over my head".

Let the stuff wash over you, and you may be surprised what sinks in - eventually :-)

Thanks again Paul.
Much appreciated ;-)

PHH


ArrayList - Searching

Before we take a look at searching with ArrayLists, let us see a few examples of the sort method in action for some basic data types.

All basic data types have a comparer defined and this is used by dotNet in both the Sort() and BinarySearch() methods.

Once we get list of objects which are more than basic data types we will need to provide out own coded comparer as in the previous couple of sub-sections.

First of all lets use the built in comparer for INT32 to sort a list of integers :-

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

Please notice that the sorted list is zero based as it is made by the system and not our code. You will get used to knowing when you have zero based arrays and when these are one based, just by getting some coding practice.

Now let us try to sort some decimal values, and here I mean decimal numbers and NOT numbers of data type Decimal.

At the time of writing these notes, and doing the live testing, there is an unresolved issue with the built-in comparer for type FLOAT. When we use decimal numbers on their own as input the system seems to take these as type FLOAT - which gives a runtime problem - so I have cast these to be REAL8 which is actually System.Double if you look in section 35 sub-section 2 you will see this clearly in the table shown.

Also see just after this for an alternative way to specify a double value - courtesy of Chris Pyrgas ;-)

So let us see how we can use the built-in comparer for 'Double' to get a sorted list :-

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

Here is the alternative way of forcing the list elements to be of type double :-

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

This could turn out to be quite useful in the dotNet world since the default of FLOAT may not always suit our needs.

Now our sort is working with our decimal values (FLOAT cast to Double) we can go that step further and use the BinarySearch() method to find our chosen 'double' value.

Lets see this in action, and remember this working code is included in the last sub-section of this main section for you to copy and paste into your IDE and try out for yourselves.

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

If we look for our friend John (guess who) in a list of string values then we get the following :-

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

What does the Binary Search produce ?

At this point I would like to draw your attention to the fact that you must first sort the list before you can use the 'Binary Search' technique successfully, and consistently.

When a binary search is done on a sorted list, a hit will be found if the target is contained in the list. A non negative result is the case of a 'hit'. If the target is not in the list a negative value is returned and the value of this is explained in the SDK - it is not simple ! However, it has to be said that the variable into which you assign your result MUST therefore be an INT and not a DWORD. You could end up with spurious results if you do not type your receiving variable correctly. I know, I have been there :-)

When you have a result (or hit) It may not be the first occurrence which is found, or the last. It is just going to be one that is there in the list.

So if you are interested in going on further, after searching, to want the first, last, or all the target items, you need to do as follows.

Once you have the position of a 'hit', go up the list till the list member is different to the target. Then step through the list the other way, until the list member is once again different. Let us try that out for ourselves on a slightly longer version of the names list above.

We will find all of the likely values associated with list searching - the first, last and the short list of similar items.

Let us start by having Harry in the list many times, right throughout the list. We we sort and display as below :-

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

A visual inspection shows us that the first position in the list is third, and the last position ninth. With seven items with value 'Harry'.

If we now ask for the binary search to be carried out for 'Harry' we will see a value of somewhere from 3 to 9 let us see what we get :-

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

So in fact we don't actually get the first or the last position in this example.

We now need to add some code lines to find out what we are after. The first and last values, then we can extract the complete sub-set called 'Harry'.

Let us see what these extra lines may look like :-

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

Well, I think we can say we have got what we are after, but you will need to be careful with little things, like making sure your bounds are correct, don't mix up your zero and one based arrays, and which index values you need to use.

If in doubt, go a step at a time, putting in message boxes to see what you have, and when anything goes wrong you will know where to look.

I have left a fair bit of code, commented out, in the working section for you to use.

Now lets look at a different data type, the DateTime type, and see how the in-built comparer works to give us the results we may expect.

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

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

I will now leave it to you to try out other types, you make try a TimeSpan for example.

Finally in this section, we need to discuss the searching for list items which are not standard types.

When we have a list of more complex items we have to provide our own comparer, but this we have already covered in the last section.

So that is good news !

If you have a compare to provide a sort, then the same comparer is used in the binary search. Just remember that you need to adjust things a bit if you wish the first, last and total items to be known, in the same way we did it above.

I will now leave it to you to try out a search on lists of objects in the last section, using the ideas above. The only complication with more complex data types is that you have to make sure you assign the correct values to the properties of the object which is the target object. And, that the comparer actually operates on the properties which are of direct interest to you.

Good Luck with your sorting and searching. With ArrayLists you can do just about anything, and they are a good substitute for the VO coding style that many used when dealing with ARRAY types and AScan() & ASort().

PHH March 2008.


SortedList - class

If you enjoyed getting to grips with the ArrayList class, you are going to love the SortedList class.

Once we appreciate that in .NET all things are objects with properties and methods, then we are on our way !

The SortedList class, like its cousin the ArrayList class, allows the developer to store a list of objects.

These objects may obviously be "customized" objects, of your choosing.

Similar to the ArrayList class, you don't need to pre-specify an array length, just add to it as you go. Very nice for VO converts I suspect.

The index starts at zero, as for the ArrayList class.

The extra functionality that SortedLists have over their cousins, is that they also have a "key" which is automatically updated by the system.

So when you give a new object to the growing list you also specify a "key".

This is very familiar for those of us who have used DBFCDX data for some time. It is like having an index on a key field and seeking.

You will see in the code supplied below that I have used the patient unique reference number as the key.

Let's look at the code now :-

METHOD MakeShowArrayList3() AS VOID

LOCAL nCount AS LONG

LOCAL cName, cFirstnames, cSurname, cText, cTitle, cPatRef, cKey AS STRING

LOCAL lActive AS LOGIC

LOCAL nPatRef, nIndex AS LONG

LOCAL oSL AS SortedList

LOCAL oPatRec AS PatientRecord

oSL := SortedList{}

oServerPA:GoTop()

nCount := 0

DO WHILE ! oServerPA:EOF

cSurname := ALLTRIM(oServerPA:fieldget("surname"))

cFirstnames := ALLTRIM(oServerPA:fieldget("firstnames"))

cTitle := ALLTRIM(oServerPA:fieldget("title"))

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

cName := cFirstnames+SPACE(1)+cSurname

nPatRef := oServerPA:fieldget("patunref")

lActive := oServerPA:fieldget("patactive")

cPatRef := nPatRef:ToString()

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

oPatRec := PatientRecord{}

oPatRec:Fullname := cName

oPatRec:Title := cTitle

oPatRec:Surname := cSurname

oPatRec:Firstnames := cFirstnames

oPatRec:PatRef := nPatRef

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

ckey := cPatRef

oSL:Add(cKey, oPatRec)

nCount +=1

IF nCount > 1313

EXIT

ENDIF

oServerPA:Skip(+1)

ENDDO

nIndex := oSL:IndexOfKey("10014")

oPatRec := (PatientRecord)oSL:GetByIndex(nIndex)

cText := "Number of elements :-"+oSL:Count:ToString()+CRLF+CRLF

cText += "Patient No. "+(nIndex+1):ToString()+" : [ customized object ]"+CRLF

cText += "----------------------------------------"+CRLF

cText += oPatRec:Fullname+CRLF

cText += oPatRec:PatRef:ToString()+CRLF

MessageBox.Show(SELF,cText,"Patient Details : from SortedList :")

RETURN

I have tried with the key as a string, and as a long integer. Both seem to work fine although the order is different, because of the alphabetic sort being different.

When you wish to retrieve patient information you may use either the index or the key approach.

To use the key value approach, you need to first use the "IndexOfKey()" method to find the index, which may then be used by the "GetByIndex()" method.

You would be well advised to consult the Framework 2.0 SDK documentation and to setup a demo app where you can try out a number of related methods and properties.

Look at Wiki to see other articles on lists.

The PatientRecord class is shown below for completeness :-

CLASS PatientRecord

EXPORT Surname, Firstnames, Fullname AS STRING

EXPORT DoB AS DATE

EXPORT Age, PatRef AS LONG

EXPORT Title AS STRING

EXPORT LastPayment AS FLOAT

CONSTRUCTOR()

SUPER()

SELF:Fullname := ""

SELF:Surname := ""

SELF:Firstnames := ""

SELF:PatRef := 0

SELF:Dob := CTOD("--/--/----")

SELF:Age := 0

SELF:Title := ""

SELF:LastPayment := 0.0

RETURN

END CLASS

Look out in Wiki for other articles on using SortedLists with more complex keys.

Also using theM in place of the ASort() and AScan()functions in Visual Objects.

PHH Oct.2007

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

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


Stacks

Stcks

Using Stacks :

The stack and queue are useful and necessary data structures, even if they are much less frequently used by the standard user and code developer.

The both appear in the System.Collections namespace, so we need :-

#USING System.Collections

at the top of our PRG code section.

It may interest you to see what the SDK has to say about the 'stack' :-

Stack is implemented as a circular buffer.

The capacity of a Stack is the number of elements the Stack can hold. The default initial capacity for a Stack is 10. As elements are added to a Stack, the capacity is automatically increased as required through reallocation.

If Count is less than the capacity of the stack, Push is an O(1) operation. If the capacity needs to be increased to accommodate the new element, Push becomes an O(n) operation, where n is Count. Pop is an O(1) operation.

Stack accepts a null reference (Nothing in Visual Basic) as a valid value and allows duplicate elements

To most of us all we need to remember of a stack is "First In Last Out" or FILO. The other way of seeing the stack universe is "Last In First Out" or LIFO. It just depends which end of the piece of string you wish to view first.

The classic example quoted is always the hot plate 'device' at the self serve restaurant, where plates removed by the self serve customers gradually reduce the stack (or pile) of plates in the warming device until the spring loaded mechanism has the last plate pushed to the top ready for use. When this is gone to accept food, the device is empty so the 'stack' of plates has been reduced to zero.

Obviously in this real world metaphor the last plate used had to be the first one to be loaded into the device. Just lets hope it comes out warm :-)

In one sense people in a lift car almost present a real life metaphor for the stack. However, there is usually one guy who likes to push his way to the front out of turn - such is life!

Enough of this chat, lets get some code down and see this structure in use.

Like the array list in the same namespace, the stack can accept objects, and data types are just objects. Let us see our first example with strings values. So we will pretend we have a lift scenario.

Starting with an empty lift we 'push' people into the lift til a required capacity is reached. At another floor the doors open and maybe a person 'pop's out of the opening doors. As the glass doored lift is moving between floors, a 'peek' will show you which member of the lift group is next to be 'popped'.

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

OK, so we have a stack with seven elements, lets do a 'peek' to see what we should get (without changing the state of the stack) and then a 'pop' to see what comes out.

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

So we have access to the last element in, 'Don' and we have not altered the state of the stack. But how to we tell about other parts of the stack ? Well we can start with seeing if a given element is contained in the stack. Lets look for "Vernon", to see if he is in the stack.

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

What about if we wished to be able to view or print, part or all of our stack without destroying it ? Don't forget that as with other classes in the System.Collections namespace, we can convert the structure to an array, and then process this object.

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

Okay, we are just about done now. If we can gain access to the array we could find the position of the elements in the stack and so on.

I suggest you look into the CopyTo() and Clone() methods to see what that has to offer, if you wish to go farther with stacks than what has been shown here.

Also don't forget that if you can create a standard array so easily as this, then you could drop it into a combo or list control on a form :-)

PHH March 2008.


Queues

Using Queues :

The Queue class is part of the System.Collections namespace so we need :-

#USING System.Collections

at the top of our PRG code section.

Working with queues is very similar to working with stacks, so many things will seem familiar after the last sub-section.

When we looked at the stack structure we just examined the instantiation with either specified or default length. However, as these classes are part of the Collections namespace the may also be created, or instantiated, from an ICollection object. Lets see if we can make our queue this way and if so remember we can also do similar with stacks too.

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

Wow ! now we start to get somewhere. We have made a queue from our stack object so the poor people who got in the lift first can have correct and proper treatment by being dealt with in order that they first got in the lift :o)

Of course we could have built up and array list in a live or interactive way, and then made the queue from that, as this too is an ICollection object. The class belongs to the Collections namespace. So we could have built up a list of documents to be printed, as an array list, and then made this into a queue and used by code to send each one in turn to the printer. Ok, Windows has a printer queue already - BUT - we are just playing with ideas in order to learn so dotNet and Vulcan syntax.

First lets see this conversion in action - for a learning experience :-

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

Let us now see how to gain access, and process, the first person see to be in our queue :-

(However, lets make the queue object first in a 'conventional' queue manner.)

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

In order to get the next element from the queue we "dequeue" it as opposed to "enqueue"ing when we add elements into the structure.

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

Right then to finish let us make a string array and use the CopyTo() method od the Queue to place the elements within it.

We will then see who was last in the queue by looking at the last element in the array.

Note also that this sort of conversion may be done with the stack structure in the previous sub-section.

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

Don't forget that with standard array you can now fill a form control based on a list, personally I like combo boxes.

This should now get you nicely on you way with using Queue and Stack structures, and converting them back and forth between one ICollections object and another. You should have lots of flexibility to do as you wish. And if still in doubt don't forget to consult the Framework SDK for help.

PHH March 2008.


working code

Converting an arraylist object to a standard array :-

/*LOCAL aListNames AS ArrayList

LOCAL cPerson, cText AS STRING

LOCAL nLen, nIndex AS DWORD

LOCAL aNames AS STRING[]

aListNames := ArrayList{}

aListNames:Add("John")

aListNames:Add("Brian")

aListNames:Add("Tom")

aListNames:Add("Dick")

aListNames:Add("Harry")

aListNames:Add("Vernon")

aListNames:Add("Fred")

aListNames:Add("Willie")

aListNames:Add("Don")

nLen := aListNames:Count

aNames := STRING[]{nLen}

nIndex := 0

aListNames:CopyTo(aNames, nIndex)

nLen := aNames:Length

cText := "New Array has length : "+nLen:ToString()

MessageBox.Show(cText,"ArrayList to Array :") */

/*

LOCAL aDT AS DateTime[]

LOCAL cText, cHead AS STRING

LOCAL oDT AS DateTime

aDT := DateTime[]{3}

aDT[1] := DateTime{2007,01,01,03,03,03}

aDT[2] := DateTime{2007,01,01,02,02,02}

aDT[3] := DateTime{2007,01,01,04,04,04}

Array.Sort(aDT)

cText := "My first element is as follows :- "+CRLF+CRLF

cText += "Short time :- "+aDT[1]:ToShortTimeString()

cHead := "Arrays (SA) : Sorting Examples :"

MessageBox.Show(cText,cHead)

/*

LOCAL aMyFiles AS FileInfo[]

LOCAL oFI AS FileInfo

LOCAL cText, cHead AS STRING

LOCAL nLen, i AS DWORD

aMyFiles := FileInfo[]{7}

oFI := FileInfo{"C:\VDNUK\patanon32.dbf"}

aMyFiles[1] := oFI

aMyFiles[2] := FileInfo{"C:\VDNUK\Amanda.wma"}

aMyFiles[3] := FileInfo{"C:\VDNUK\QC.bmp"}

aMyFiles[4] := FileInfo{"C:\VDNUK\FiveIcons.jpg"}

aMyFiles[5] := FileInfo{"C:\VDNUK\GreatBearLogo.jpg"}

aMyFiles[6] := FileInfo{"C:\VDNUK\Pat32.dbf"}

aMyFiles[7] := FileInfo{"C:\VDNUK\Pat32.cdx"}

Array.Sort(aMyFiles,MyFilesMostRecentFirst{})

cText := "My modified files, latest first are :- "+CRLF+CRLF

FOR i := 1 UPTO aMyFiles:Length

cText += "value >> "+aMyFiles[i]:LastWriteTime:ToString()+CRLF

NEXT

cHead := "Arrays (SA) : Custom Sorting :"

MessageBox.Show(cText,cHead)

*/

/*

LOCAL aMyFiles AS FileInfo[]

LOCAL oFI AS FileInfo

LOCAL cText, cHead AS STRING

LOCAL nLen, i AS DWORD

aMyFiles := FileInfo[]{7}

oFI := FileInfo{"C:\VDNUK\patanon32.dbf"}

aMyFiles[1] := oFI

aMyFiles[2] := FileInfo{"C:\VDNUK\Amanda.wma"}

aMyFiles[3] := FileInfo{"C:\VDNUK\QC.bmp"}

aMyFiles[4] := FileInfo{"C:\VDNUK\FiveIcons.jpg"}

aMyFiles[5] := FileInfo{"C:\VDNUK\GreatBearLogo.jpg"}

aMyFiles[6] := FileInfo{"C:\VDNUK\Pat32.dbf"}

aMyFiles[7] := FileInfo{"C:\VDNUK\Pat32.cdx"}

Array.Sort(aMyFiles,MyFilesSmall2Large{})

cText := "My file sizes S - L are :- "+CRLF+CRLF

FOR i := 1 UPTO aMyFiles:Length

cText += "value >> "+aMyFiles[i]:Length:ToString()+CRLF

NEXT

cHead := "Arrays (SA) : Custom Sorting :"

MessageBox.Show(cText,cHead)

*/

/*

LOCAL aMyFiles AS FileInfo[]

LOCAL oFI AS FileInfo

LOCAL cText, cHead AS STRING

LOCAL nLen, i AS DWORD

aMyFiles := FileInfo[]{7}

oFI := FileInfo{"C:\VDNUK\patanon32.dbf"}

aMyFiles[1] := oFI

aMyFiles[2] := FileInfo{"C:\VDNUK\Amanda.wma"}

aMyFiles[3] := FileInfo{"C:\VDNUK\QC.bmp"}

aMyFiles[4] := FileInfo{"C:\VDNUK\FiveIcons.jpg"}

aMyFiles[5] := FileInfo{"C:\VDNUK\GreatBearLogo.jpg"}

aMyFiles[6] := FileInfo{"C:\VDNUK\Pat32.dbf"}

aMyFiles[7] := FileInfo{"C:\VDNUK\Pat32.cdx"}

//Array.Sort(aMyFiles,MyFilesAZ{})

Array.Sort(aMyFiles,MyFilesSmall2Large{})

cText := "My file sizes S - L are :- "+CRLF+CRLF

//cText := "My file names A - Z are :- "+CRLF+CRLF

FOR i := 1 UPTO aMyFiles:Length

cText += "value >> "+aMyFiles[i]:ToString()+CRLF

cText += "value >> "+aMyFiles[i]:ToString()+CRLF

NEXT

cHead := "Arrays (SA) : Custom Sorting :"

MessageBox.Show(cText,cHead)

*/

/*

LOCAL aNames AS STRING[]

LOCAL cText, cHead AS STRING

LOCAL nLen, i AS DWORD

aNames := STRING[]{6}

aNames[1] := "Tom"

aNames[2] := "Dick"

aNames[3] := "Harry"

aNames[4] := "Mary"

aNames[5] := "Anne"

aNames[6] := "Sally"

nLen := aNames:Length

Array.Sort(aNames)

Array.Reverse(aNames)

cText := "my array names Z-A are :- "+CRLF+CRLF

FOR i := 1 UPTO nLen

cText += Space(5)+aNames[i]+CRLF

NEXT // i

cHead := "Arrays (SA) : Sorting strings : "

MessageBox.Show(cText, cHead)

*/

/*

LOCAL cText1, cText2, cHead AS STRING

LOCAL aValues AS FLOAT[]

LOCAL nPosn AS LONG

LOCAL fTarget AS FLOAT

LOCAL i AS DWORD

aValues := FLOAT[]{7}

aValues[1] := -1.1

aValues[2] := 202.02

aValues[3] := 31.2

aValues[4] := 0.13

aValues[5] := 5.0

aValues[6] := -33.3

aValues[7] := 1.1

Array.Sort(aValues,MyFloatBS{})

cText1 := "My highest / lowest FLOATS are :- "+CRLF+CRLF

FOR i := 1 UPTO 7

cText1 += "value >> "+aValues[i]:ToString()+CRLF

NEXT // i

fTarget := 5.0

nPosn := 1+Array.BinarySearch(aValues,fTarget,MyFloatBS{})

//----- need to add one for zero to one base arrays -----

cText2 := "first binary search results :- "+CRLF+CRLF

cText2 += "Index :- "+nPosn:ToString()+CRLF

cText2 += "Value found :- "+aValues[nPosn]:ToString()+CRLF

cHead := "Arrays (SA) : Binary Search :"

MessageBox.Show(cText1+CRLF+cText2, cHead)

*/

/*

LOCAL cText, cHead AS STRING

LOCAL aValues AS LONG[]

aValues := LONG[]{7}

aValues[1] := 3

aValues[2] := 12

aValues[3] := 302

aValues[4] := 13

aValues[5] := -5

aValues[6] := 33

aValues[7] := -45

Array.Sort(aValues)

cText := "My highest / lowest long integers are :- "+CRLF+CRLF

cText += "value >> "+aValues[7]:ToString()+CRLF

cText += "value >> "+aValues[1]:ToString()

cHead := "Arrays (SA) : Sorting Examples :"

MessageBox.Show(cText,cHead)

*/

/*LOCAL aListNums AS ArrayList

LOCAL cText, cHead AS STRING

LOCAL nLen AS DWORD

aListNums := ArrayList{}

aListNums:Add(3)

aListNums:Add(2)

aListNums:Add(1)

aListNums:Add(8)

aListNums:Add(6)

aListNums:Add(5)

aListNums:Sort()

nLen := aListNums:Count

cText := ""

cHead := "Sort Result :"

cText += "Smallest :- "+aListNums[0]:ToString()+CRLF

cText += "Largest :- "+aListNums[nLen-1]:ToString()+CRLF

MessageBox.Show(cText, cHead) */

/*LOCAL aListNums AS ArrayList

LOCAL cText, cHead AS STRING

LOCAL nLen AS DWORD

aListNums := ArrayList{}

aListNums:Add((REAL8)3.33)

aListNums:Add((REAL8)2.22)

aListNums:Add((REAL8)1.11)

aListNums:Add((REAL8)8.88)

aListNums:Add((REAL8)6.66)

aListNums:Add((REAL8)5.55)

aListNums:Sort()

nLen := aListNums:Count

cText := ""

cHead := "Sort Result :"

cText += "Smallest :- "+aListNums[0]:ToString()+CRLF

cText += "Largest :- "+aListNums[nLen-1]:ToString()+CRLF

MessageBox.Show(cText, cHead) */

/*LOCAL aListNums AS ArrayList

LOCAL cText, cHead AS STRING

LOCAL nLen AS DWORD

LOCAL nResult AS INT //------ important for negative results of search ------

aListNums := ArrayList{}

aListNums:Add((REAL8)3.33)

aListNums:Add((REAL8)2.22)

aListNums:Add((REAL8)1.11)

aListNums:Add((REAL8)8.88)

aListNums:Add((REAL8)6.66)

aListNums:Add((REAL8)5.55)

aListNums:Sort()

nLen := aListNums:Count

nResult := aListNums:BinarySearch((REAL8)5.55)

cText := ""

cHead := "Search Result :"

cText += "position is :- "+nResult:ToString()+e"\n\n"

cText += "number is :- "+aListNums[nResult]:ToString()+e"\n\n"

cText += e"[ zero based after sort! ]\n"

MessageBox.Show(cText, cHead) */

/*LOCAL aListNames AS ArrayList

LOCAL cText, cHead AS STRING

LOCAL nLen AS DWORD

LOCAL nResult AS INT //------ important for negative results of search ------

aListNames := ArrayList{}

aListNames:Add("Tom")

aListNames:Add("Dick")

aListNames:Add("John")

aListNames:Add("Harry")

aListNames:Add("Vernon")

aListNames:Add("Fred")

aListNames:Add("Jake")

aListNames:Sort()

nLen := aListNames:Count

nResult := aListNames:BinarySearch("John")

cText := ""

cHead := "Search Result :"

cText += "position is :- "+nResult:ToString()+e"\n\n"

cText += "name is :- "+aListNames[nResult]:ToString()+e"\n\n"

cText += e"[ zero based after sort! ]\n"

MessageBox.Show(cText, cHead)

*/

/*

LOCAL dtTarget, oDT AS DateTime

LOCAL oTS AS TimeSpan

LOCAL aListDates AS ArrayList

LOCAL nFound AS INT //----- important for neg search results -----

oDT := DateTime.Now

aListDates := ArrayList{}

aListDates:Add(oDT:AddDays(3))

aListDates:Add(oDT:AddDays(2))

aListDates:Add(oDT)

aListDates:Add(oDT:AddDays(7))

aListDates:Add(oDT:AddDays(5))

aListDates:Sort()

//dtTarget := oDT

dtTarget := oDT:AddDays(1)

nFound := aListDates:BinarySearch(dtTarget)

IF nFound < 0

MessageBox.Show("Not Found : "+nFound:ToString(),"Search DateTime :")

ELSE

MessageBox.Show("Search Position : "+nFound:ToString(),"Search DateTime :")

ENDIF */

/*

LOCAL aListNames AS ArrayList

LOCAL cText, cHead, cTarget AS STRING

LOCAL nLen,i AS DWORD

LOCAL nResult AS INT //------ important for negative results of search ------

aListNames := ArrayList{}

aListNames:Add("Harry") //------ seven Haray items ------

aListNames:Add("Tom")

aListNames:Add("Harry")

aListNames:Add("Dick")

aListNames:Add("Harry")

aListNames:Add("John")

aListNames:Add("Harry")

aListNames:Add("Vernon")

aListNames:Add("Harry")

aListNames:Add("Fred")

aListNames:Add("Harry")

aListNames:Add("Jake")

aListNames:Add("Harry")

aListNames:Sort()

nLen := aListNames:Count

cText := ""

cHead := "Sorted List :"

FOR i := 0 UPTO nLen-1

cText += aListNames[i]:ToString()+CRLF

NEXT

MessageBox.Show(cText, cHead)

//nResult := aListNames:BinarySearch("Harry")

/*cText := ""

cHead := "Search Result :"

cText += "position is :- "+nResult:ToString()+e"\n\n"

cText += "name is :- "+aListNames[nResult]:ToString()+e"\n\n"

cText += e"[ zero based after sort! ]\n" */

cTarget := "Harry"

nResult := aListNames:BinarySearch(cTarget)

//MessageBox.Show(nResult:ToString())

LOCAL nLast, nFirst, nTotal AS DWORD

LOCAL aListSubSet AS ArrayList

nFirst := nResult

FOR i := nResult DOWNTO 0

IF !aListNames[i] == cTarget

nFirst := i+1

EXIT

ENDIF

NEXT

//MessageBox.Show(nFirst:ToString())

nLast := nResult

FOR i := nResult UPTO nLen-1

IF !aListNames[i] == cTarget

nLast := i-1

EXIT

ENDIF

NEXT

//MessageBox.Show(nLast:ToString())

aListSubSet := ArrayList{}

FOR i := nFirst UPTO nLast

aListSubSet:Add(aListNames[i])

NEXT

nTotal := aListSubSet:Count

cText := ""

cHead := "Harry Sub-set :"

cText += "First : "+nFirst:ToString()+CRLF

cText += "Last : "+nLast:ToString()+CRLF

cText += "Total : "+nTotal:ToString()+CRLF

MessageBox.Show(cText, cHead)

*/

/*LOCAL aListNums AS ArrayList

LOCAL cText, cHead AS STRING

LOCAL nLen AS DWORD

LOCAL nResult AS INT //------ important for negative results of search ------

aListNums := ArrayList{}

aListNums:Add(3.33m)

aListNums:Add(2.22m)

aListNums:Add(1.11m)

aListNums:Add(8.88m)

aListNums:Add(6.66m)

aListNums:Add(5.55m)

aListNums:Sort()

nLen := aListNums:Count

nResult := aListNums:BinarySearch(5.55m)

cText := ""

cHead := "Search Result :"

cText += "position is :- "+nResult:ToString()+e"\n\n"

cText += "number is :- "+aListNums[nResult]:ToString()+e"\n\n"

cText += e"[ zero based after sort! ]\n"

MessageBox.Show(cText, cHead) */

STACK:

LOCAL sLift AS Stack

LOCAL nTotal AS DWORD

sLift := Stack{} //-------- alternative e.g. sLift := Stack{33} --------

sLift:Push("Tom")

sLift:Push("Dick")

sLift:Push("Harry")

sLift:Push("Vernon")

sLift:Push("Fred")

sLift:Push("Willie")

sLift:Push("Don")

nTotal := sLift:Count

MessageBox.Show("Number in the stack :- "+nTotal:ToString(),"Stack tesing :")

/*LOCAL sLift AS Stack

LOCAL nTotal, nAfter, nBefore AS DWORD

LOCAL cPerson, cText AS STRING

sLift := Stack{} //-------- alternative e.g. sLift := Stack{33} --------

sLift:Push("Tom")

sLift:Push("Dick")

sLift:Push("Harry")

sLift:Push("Vernon")

sLift:Push("Fred")

sLift:Push("Willie")

sLift:Push("Don")

nBefore := sLift:Count

cPerson := (STRING)sLift:Peek()

nAfter := sLift:Count

cText := "before : "+nBefore:ToString()+CRLF

cText += "next is : "+cPerson+CRLF

cText += "after : "+nAfter:ToString()+CRLF

MessageBox.Show(cText,"Stack tesing :") */

/*LOCAL sLift AS Stack

LOCAL cPerson, cText AS STRING

LOCAL lResult AS Boolean

sLift := Stack{} //-------- alternative e.g. sLift := Stack{33} --------

sLift:Push("Tom")

sLift:Push("Dick")

sLift:Push("Harry")

sLift:Push("Vernon")

sLift:Push("Fred")

sLift:Push("Willie")

sLift:Push("Don")

cPerson := "Vernon"

lResult := sLift:Contains(cPerson)

cText := "Search for : "+cPerson

cText += e"\nresult was : "+lResult:ToString()

MessageBox.Show(cText,"Stack tesing :") */

/*LOCAL sLift AS Stack

LOCAL cPerson, cText AS STRING

LOCAL lResult AS Boolean

LOCAL aStack AS OBJECT[]

LOCAL nLen, i AS DWORD

sLift := Stack{}

sLift:Push("Tom")

sLift:Push("Dick")

sLift:Push("Harry")

sLift:Push("Vernon")

sLift:Push("Fred")

sLift:Push("Willie")

sLift:Push("Don")

aStack := sLift:ToArray()

nLen := aStack:Length

IF nLen > 0

cText := ""

FOR i := 1 UPTO nLen

cText += e"\n\t"+(STRING)aStack[i]

NEXT

ELSE

cText := "empty array exists !"

ENDIF

MessageBox.Show(cText,"Stack to Array :")

*/

QUEUE:

/*LOCAL sLift AS Stack

LOCAL cPerson, cText AS STRING

LOCAL qLift AS Queue

sLift := Stack{}

sLift:Push("Tom")

sLift:Push("Dick")

sLift:Push("Harry")

sLift:Push("Vernon")

sLift:Push("Fred")

sLift:Push("Willie")

sLift:Push("Don")

qLift := Queue{sLift}

cText := "Queue length : "+qLift:Count:ToString()

MessageBox.Show(cText,"Stack to Queue :") */

LOCAL qLift AS Queue

LOCAL aListLift AS ArrayList

LOCAL cPerson, cText AS STRING

LOCAL nLen AS DWORD

aListLift := ArrayList{}

aListLift:Add("John")

aListLift:Add("Brian")

aListLift:Add("Tom")

aListLift:Add("Dick")

aListLift:Add("Harry")

aListLift:Add("Vernon")

aListLift:Add("Fred")

aListLift:Add("Willie")

aListLift:Add("Don")

qLift := Queue{aListLift}

nLen := qLift:Count

cText := "New Queue has length : "+nLen:ToString()

MessageBox.Show(cText,"ArrayList to Queue :")

LOCAL qLift AS Queue

LOCAL cPerson, cText AS STRING

LOCAL nLen AS DWORD

qLift := Queue{}

qLift:Enqueue("Brian")

qLift:Enqueue("Chris")

qLift:Enqueue("Paul")

qLift:Enqueue("Ginny")

qLift:Enqueue("Willie")

qLift:Enqueue("Ivo")

nLen := qLift:Count

cText := "New Queue has length : "+nLen:ToString()

MessageBox.Show(cText,"Enqueue to Create :")

LOCAL qLift AS Queue

LOCAL cPerson, cText AS STRING

LOCAL nLen AS DWORD

qLift := Queue{}

qLift:Enqueue("Brian")

qLift:Enqueue("Chris")

qLift:Enqueue("Paul")

qLift:Enqueue("Ginny")

qLift:Enqueue("Willie")

qLift:Enqueue("Ivo")

nLen := qLift:Count

cPerson := qLift:Dequeue():ToString()

cText := "Next person in the queue is : "+cPerson

MessageBox.Show(cText,"Who's next please ? :")

LOCAL qLift AS Queue

LOCAL cPerson, cText AS STRING

LOCAL nLen AS DWORD

LOCAL aLift AS STRING[]

LOCAL nIndex AS INT

qLift := Queue{}

qLift:Enqueue("Brian")

qLift:Enqueue("Chris")

qLift:Enqueue("Paul")

qLift:Enqueue("Ginny")

qLift:Enqueue("Willie")

qLift:Enqueue("Ivo")

nLen := qLift:Count

aLift := STRING[]{nLen}

nIndex := 0

qLift:CopyTo(aLift, nIndex)

nLen := aLift:Length

cPerson := aLift[nLen]

cText := "Length of our new array is : "+nLen:ToString()

cText += e"\n\n\tpresenter is : "+cPerson

MessageBox.Show(cText,"Now CopyTo() array :")

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

LOCAL aTest AS Decimal[]

LOCAL i, nLen, nResult AS DWORD

LOCAL nTest, nTarget AS Decimal

LOCAL oTS1, oTS2 AS TimeSpan

LOCAL oDT1, oDT2 AS DateTime

LOCAL cTimeTaken AS STRING

nTest := 3.333m

//nTarget := nTest+454545*0.00001m

nTarget := nTest+998998*0.00001m

aTest := Decimal[]{999999}

nLen := aTest:Length

//FOR i := 1 UPTO 999999

FOR i := 999999 DOWNTO 1

aTest[i] := nTest+i*0.00001m

NEXT

cTimeTaken := ""

oDT1 := DateTime.Now

Array.Sort(aTest)

oDT2 := DateTime.Now

oTS1 := oDT2-oDT1

cTimeTaken += oTS1:TotalMilliseconds:ToString()+CRLF

MessageBox.Show(cTimeTaken,"Decimal sorted !")

cTimeTaken := ""

oDT1 := DateTime.Now

nResult := Array.BinarySearch(aTest, nTarget)

oDT2 := DateTime.Now

oTS1 := oDT2-oDT1

cTimeTaken += oTS1:TotalMilliseconds:ToString()+CRLF

//MessageBox.Show(cTimeTaken, nResult:ToString())

oDT1 := DateTime.Now

nResult := Array.IndexOf(aTest, nTarget)

oDT2 := DateTime.Now

oTS1 := oDT2-oDT1

cTimeTaken += oTS1:TotalMilliseconds:ToString()+CRLF

//MessageBox.Show(cTimeTaken, nResult:ToString())

oDT1 := DateTime.Now

nResult := Array.BinarySearch(aTest, nTarget)

oDT2 := DateTime.Now

oTS1 := oDT2-oDT1

cTimeTaken += oTS1:TotalMilliseconds:ToString()+CRLF

//MessageBox.Show(cTimeTaken, nResult:ToString())

oDT1 := DateTime.Now

nResult := Array.IndexOf(aTest, nTarget)

oDT2 := DateTime.Now

oTS1 := oDT2-oDT1

cTimeTaken += oTS1:TotalMilliseconds:ToString()+CRLF

//MessageBox.Show(cTimeTaken, nResult:ToString())

MessageBox.Show(cTimeTaken,"Decimal type :")

/*LOCAL aTest AS REAL8[]

LOCAL aTest2 AS Decimal[]

LOCAL i, nLen, nResult AS DWORD

LOCAL nTest, nTarget AS REAL8

LOCAL nTest2, nTarget2 AS Decimal

LOCAL oTS1, oTS2 AS TimeSpan

LOCAL oDT1, oDT2 AS DateTime

LOCAL cTimeTaken AS STRING

nTest := 3.333

//nTarget := nTest+12

//nTarget := nTest+121212

nTarget := nTest+454545

//nTarget := nTest+998998

aTest := REAL8[]{999999}

nLen := aTest:Length

//FOR i := 1 UPTO 999999

FOR i := 999999 DOWNTO 1

aTest[i] := nTest+i

NEXT

cTimeTaken := ""

oDT1 := DateTime.Now

Array.Sort(aTest)

oDT2 := DateTime.Now

oTS1 := oDT2-oDT1

cTimeTaken += oTS1:TotalMilliseconds:ToString()+CRLF

MessageBox.Show(cTimeTaken,"REAL8 sorted !")

cTimeTaken := ""

oDT1 := DateTime.Now

nResult := Array.BinarySearch(aTest, nTarget)

oDT2 := DateTime.Now

oTS1 := oDT2-oDT1

cTimeTaken += oTS1:TotalMilliseconds:ToString()+CRLF

//MessageBox.Show(cTimeTaken, nResult:ToString())

oDT1 := DateTime.Now

nResult := Array.IndexOf(aTest, nTarget)

oDT2 := DateTime.Now

oTS1 := oDT2-oDT1

cTimeTaken += oTS1:TotalMilliseconds:ToString()+CRLF

//MessageBox.Show(cTimeTaken, nResult:ToString())

oDT1 := DateTime.Now

nResult := Array.BinarySearch(aTest, nTarget)

oDT2 := DateTime.Now

oTS1 := oDT2-oDT1

cTimeTaken += oTS1:TotalMilliseconds:ToString()+CRLF

//MessageBox.Show(cTimeTaken, nResult:ToString())

oDT1 := DateTime.Now

nResult := Array.IndexOf(aTest, nTarget)

oDT2 := DateTime.Now

oTS1 := oDT2-oDT1

cTimeTaken += oTS1:TotalMilliseconds:ToString()+CRLF

//MessageBox.Show(cTimeTaken, nResult:ToString())

MessageBox.Show(cTimeTaken,"Using REAL8 :")

*/

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

PHH March 2008.


Section 3 - Printing

Printing - the Necessary Evil !?

Whether we like it or hate it, printing in a necessary fact of real life. For most developers, customers demand it.

My customers require control over printers as well as a variety of customised documents mixing text and data from file.

Oh, and just to add a finishing touch signatures and company logos too, both from disk file. And don't forget, I here the IT Manager saying, we want to scan and manage the signatures ourselves, through a screen and system you provide in VO.

I have a feeling that many developers use third party solutions because they are running away from doing it themselves. They think it is difficult, or even impossible.

Whereas in fact it is possible, very possible, not difficult to understand, but fiddly to set up and get right at first. It is not a task you can rush at, or get done quickly, but, if prepared correctly a basic printing class can serve you very well and allow you to do anything at all.

When I came to Visual Objects a few years ago I was fortunate enough to have as my 'bible' a book called "Exploring CA-Visual Objects 2.5" - see below. I worked my way through these interesting - but demanding - chapters to be where I am today. On my way I spent a great deal of time on Chapter 15, called - "The Printer Class - Cinderella Class ?".

This chapter was written by Alwyn (Jennings-Bramly), one of the co-authors, and is the complete basis of what I have in my Vulcan "BasicDocument" class.

Initially I wrote a VO class called "BasicPrinter" and followed very much what was on offer in the book. Later after purchasing Alwyn's "VOcab" printing solution in VO I read his comprehensive notes and got more ideas for enhancing my class and doing tidy reports using proportionally spaced fonts.

Although I achieved a lot in VO, there was one big drawback and that was the lack of a suitable and readily available print preview facility. I was lucky enough to discover PDF995 which was a great substitute, and allowed me to preview and capture what VO had printed.

The great news in VDN is that dotNet has an 'in-built' print preview facility which we will see later.

If you wish to know the truth, I came to Vulcan as a novice and set myself the task of converting my BasicPrinter class, as the first problem to solve. Seems so stupid now, however, the idea was - if I could not do my printing in Vulcan then there was no point in moving from VO. I was probably looking for an excuse to stay in my comfort zone !!! Does thay sound familiar to you ?

I am glad I did, it took me far less time than I expected, and once I hade my sample letter in the preview screen I had no excuse at all why I shouldn't go on and conquer the rest of the Vulcan and dotNet world.

I am glad I did. I don't wish to go back now, and I intend to look for all new dotNet ways of doing things, not resort to falling back into my comfort zone and use VO style tools.

A big thanks has to go to Alwyn for his inspiration and much hard work in doing what he has achieved in printing with VO.

I do NOT intend to give you anything but a 'taster' of what can be done by yourself, and your own coding, in these 'ClickStartVDN' help notes. I would seem more appropriate to provide you with a specific set of Help & Manual notes which covers just printing in Vulcan.

I will attempt to do this by March and have it included on the CD for DevFest if John Parker passes it as fit for attendee consumption.

PHH Dec.2007


BasicDocument class

BasicDocument class - the Building Blocks to Printing !

Below is shown the complete "BasicDocument" class as it now is. Don't be alarmed by what looks like a complex, or daunting structure. In fact it is surprisingly small for what it can do, and many of the methods are actually quite small.

Standing back from it, it is seen to be a 'set' of building blocks. Which sometimes actually use other building blocks themselves.

What became apparent, in early days of my printing experience, is that although it is relatively easy to get text and objects on a page to be physically printed out, it was much more difficult to 'control' the printing process as far as presenting the data to be printed was concerned. This will need to be explained more fully later.

If you 'cleanly' separate out the task of controlling your page layout, handling your data, and then managing your pages, the overall task gets much easier to get right.

Have a quick look down the method names in the image below and see that although some may need explanation, they seem refer to fundamental operations of placing text or images on a page.

At this point I would like to say that in moving over my code from VO to Vulcan I did pinpoint the places where it was appropriate to use dotNet classes and drop using VO ones.

So as far as I can tell I am doing my printing as much the dotNet way as possible.

Many of the methods listed above are actually to do with placing text at specific places on a page, governed by line number and position across the page. Although it takes time to set up tools in this way, it is much more user friendly when it comes to the task of laying out a letter.

Before you can have building block methods which place text by line down and a percentage value across the page, you need to provide more basic building block methods which place by floating point coordinates X & Y. And as we know X & Y make a point object too.

It is probably time to move on to the next sub-section to see just what sort of letter we can produce, and how the code looks to produce it.

PHH December 2007


Sample Letter

Producing Quality Print Out :

Below is an example of the test letter. It has no confidential information included on it, just publicly available company details.

Note that all elements on the page have been placed there by the methods of the BasicDocument class. The logo and the signature come directly from BMP files on disk.

You may also like to notice that the screen displaying the sample letter is actually an instance of the PrintPreview dialog class. This may 'float' and resize on the screen as well as the tool bar icons allowing a few simple functional extras. All this is a great step forward to what we had available as standard in VO.

It may be of interest to extract from the code that displays the letter, just which methods have been used from the whole set available. I mean used by the developer to code the letter, not which others are called subsequently by the methods themselves.

In the DisplayDoc method of the 'DemoDocRSpatient' class shown in the working code sub-section, we can see the following :-

oBasicDoc:BitMapPlace()

oBasicDoc:BlockOfText()

oBasicDoc:DoubleBox()

oBasicDoc:LineTextPrint()

oBasicDoc:MemoPlaceFJ()

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

oBasicDoc:Font :=

oBasicDoc:Brush :=

oBasicDoc:Pen :=

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

So we have created a fine looking letter by using only five of the 'building block' methods of the BasicDocument class.

We have also set three properties of that class, and used the 'font' the most, as we changed from one section of the letter to another.

Notice the MemoPlaceFJ() method acually places a paragraph on the page and fully justifies the text at the left and right sides.

Please notice that I have completely separated out the function of 'getting' the data to be printed, from the task of placing the items on the page. To me, one of the secrets of good printing, and easy code maintenance.

Obviously, the BitMapPlace method puts both the logo and the signature on the page.

The DoubleBox method seems obvious too, this makes the purple box. However, this is a good example where one building block calls another, as DoubleBox() uses SingleBoxLineRealAcrossReal() which uses SingleBox(). It is only then that the SingleBox() method calls the dotNet graphics method 'DrawLine'. These three methods are included at the end of the working code section to illustrate this point.

So most of my method code is about controlling placement and not actual drawing !

Finally on this sub-section, if you care to view the working code you will see that the method to find the data is just a simple work-round for most of the data not being available from data file on disk. In reality, the live and non-demo letters would have company, patient, and product item data retrieved from data file.

However, I still load up my arrays first, so that column placing of the item details in the mid-section is coded simply in the letter layout method.

Keeping it clean like this means we can have a number of different style documents being printed to different printers at the same time. No problems!

PHH December 2007.


Working Code Sample

CLASS DemoDocRSpatient

EXPORT Patient AS STRING

EXPORT SigFilePath, PelicanLogo AS STRING

EXPORT CompanyDetails AS ARRAY

EXPORT Directors AS ARRAY

PROTECT PelHead AS STRING

PROTECT DateRef AS ARRAY

PROTECT Items, Descriptions, Packs, Qtys, ToFs AS ARRAY

PROTECT Para1, Para2, Para3, Para4 AS STRING

PROTECT Sender, SenderTitle, LetterRef, ToWhom AS STRING

PROTECT oBasicDoc AS BasicDocument

CONSTRUCTOR (cPatient AS STRING, cSignature AS STRING) CLASS DemoDocRSpat

SELF:Patient := AllTrim(cPatient)

SELF:SigFilePath := AllTrim(cSignature)

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

//SELF:PelicanLogo := "c:\VIDE\projects\BasicVulcanPrinting\PelLogo1b.bmp"

SELF:PelicanLogo := "c:\VDNUK\PelLogo1b.bmp"

//SELF:SigFilePath := "c:\VIDE\projects\BasicVulcanPrinting\SBugler.bmp"

SELF:SigFilePath := "c:\VDNUK\SBugler.bmp"

SELF:Patient := "Mr. A Patient"

SELF:FindData()

RETURN

METHOD FindData() AS VOID

//------------- most of this would normally be pulled from DBFs on the network ------------

//------------- only the actual paragraph wording is coded as such ------------------------

LOCAL cPara, cFree AS STRING

SELF:CompanyDetails := {}

AAdd(SELF:CompanyDetails, "Pelican Healthcare Ltd")

AAdd(SELF:CompanyDetails, "Cardiff Business Park")

AAdd(SELF:CompanyDetails, "Cardiff CF14 5WF UK")

AAdd(SELF:CompanyDetails, "Tel: 029 2074 7000")

AAdd(SELF:CompanyDetails, "Fax: 029 2074 7001")

AAdd(SELF:CompanyDetails, "E-mail: mailroom@pelicanhealthcare.co.uk")

AAdd(SELF:CompanyDetails, "Website: www.pelicanhealthcare.co.uk")

SELF:PelHead := "PELICAN HOME DELIVERY SERVICE"

SELF:DateRef := {}

AAdd(SELF:DateRef,"Date : 24/04/2007")

AAdd(SELF:DateRef,"Our ref : 1622 235754")

SELF:ToWhom := "Dear "+SELF:Patient+","

cPara := "Please find enclosed the items from your Prescription request. Items marked 'to follow' will be despatched shortly. "

SELF:Para1 := cPara

cPara := "Please could you forward on your Prescription in the Freepost envelope provided if you have not already done so. "

cPara += "Alternatively, if you have requested us to collect your prescription for you, please ensure that you complete and sign "

cPara += "the Registration Card enclosed, and return it to us as soon as possible. We will then obtain your Prescription."

SELF:Para2 := cPara

cPara := "Please find enclosed your complimentary Luxury Washbag, filled with a range of accessories for you to try. "

cPara += " The complimentary Moist Wipes, Dry Wipes and Disposal Bags are sent to you with every order."

SELF:Para3 := cPara

cFree := "0800 052 7471"

cPara := "We pride ourselves on providing you with a friendly, fast and efficient service. Should you require any "

cPara += "further assistance, or have any queries at all regarding the above, then please do not hesitate to give me a call"

cPara += " on the Freephone Number: "+cFree+" and I will be happy to help. "

cPara += "Assuring you of our full attention at all times."

SELF:Para4 := cPara

SELF:Directors := {}

AAdd(SELF:Directors, "Directors P. Hale - N. Hale - N.R. Jones - H.K. Clark")

AAdd(SELF:Directors, "Incorporated England No 2648823 - VAT No GB615508844")

SELF:LetterRef := "Ref : 24 - P1 - R"

SELF:Sender := "Shelly Bugler"

SELF:SenderTitle := "Prescription Clerk"

SELF:Items := {}

AAdd(SELF:Items, "Item")

AAdd(SELF:Items, "Code")

AAdd(SELF:Items, "===")

AAdd(SELF:Items, "Z7032")

AAdd(SELF:Items, "Z7031")

AAdd(SELF:Items, "S106")

AAdd(SELF:Items, "MTBAG")

AAdd(SELF:Items, "823-15")

AAdd(SELF:Items, "3905")

AAdd(SELF:Items, "3500")

AAdd(SELF:Items, "3346P")

AAdd(SELF:Items, "3345P")

SELF:Descriptions := {}

AAdd(SELF:Descriptions, "")

AAdd(SELF:Descriptions, "Description")

AAdd(SELF:Descriptions, "===========")

AAdd(SELF:Descriptions, "Pelican New Dry Wipes")

AAdd(SELF:Descriptions, "Disposal Bags")

AAdd(SELF:Descriptions, "Orahesive Powder")

AAdd(SELF:Descriptions, "MALE TOILET BAG")

AAdd(SELF:Descriptions, "Nova Drainable Bag")

AAdd(SELF:Descriptions, "Limone spray")

AAdd(SELF:Descriptions, "Appeel Adh Remover")

AAdd(SELF:Descriptions, "Cavilon Barrier Film")

AAdd(SELF:Descriptions, "Cavilon Barrier Film")

SELF:Packs := {}

AAdd(SELF:Packs, "Pack")

AAdd(SELF:Packs, "Size")

AAdd(SELF:Packs, "====")

AAdd(SELF:Packs, "30")

AAdd(SELF:Packs, "30")

AAdd(SELF:Packs, "25g")

AAdd(SELF:Packs, "1")

AAdd(SELF:Packs, "30")

AAdd(SELF:Packs, "50ml")

AAdd(SELF:Packs, "50ml")

AAdd(SELF:Packs, "28ml")

AAdd(SELF:Packs, "5x3ml")

SELF:Qtys := {}

AAdd(SELF:Qtys, "Qty.")

AAdd(SELF:Qtys, "Sent")

AAdd(SELF:Qtys, "====")

AAdd(SELF:Qtys, " 1")

AAdd(SELF:Qtys, " 2")

AAdd(SELF:Qtys, " 3")

AAdd(SELF:Qtys, " 4")

AAdd(SELF:Qtys, " 5")

AAdd(SELF:Qtys, " 6")

AAdd(SELF:Qtys, " 7")

AAdd(SELF:Qtys, " 8")

AAdd(SELF:Qtys, " 9")

SELF:ToFs := {}

AAdd(SELF:ToFs, "To")

AAdd(SELF:ToFs, "Follow")

AAdd(SELF:ToFs, "======")

AAdd(SELF:ToFs, "")

AAdd(SELF:ToFs, "")

AAdd(SELF:ToFs, " 1")

AAdd(SELF:ToFs, "")

AAdd(SELF:ToFs, "")

AAdd(SELF:ToFs, " 2")

AAdd(SELF:ToFs, "")

AAdd(SELF:ToFs, "")

AAdd(SELF:ToFs, " 1")

RETURN

METHOD DisplayDoc(oBasicDoc AS BasicDocument) AS VOID

LOCAL oFont9CS, oFont11CS, oFont10A, oFont12A, oFont8A, oFont22CS AS Font

LOCAL nDown, nAcross, nAA AS LONG

LOCAL cPara, cText AS STRING

oFont12A := Font{"Arial",12}

oFont10A := Font{"Arial",10}

oFont8A := Font{"Arial",8}

oFont9CS := Font{"Comic Sans MS",9}

oFont11CS := Font{"Comic Sans MS",11}

oFont22CS := Font{"Comic Sans MS",22}

oBasicDoc:Font := oFont11CS

oBasicDoc:Brush := SolidBrush{Color.Black}

oBasicDoc:Pen := Pen{oBasicDoc:Brush, 1.0}

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

nDown := 16

nAcross := 8

//_wait(oBasicDoc:MyPrinterName)

oBasicDoc:BitMapPlace(SELF:PelicanLogo, 1, 75)

oBasicDoc:Font := oFont10A

oBasicDoc:BlockOfText(SELF:CompanyDetails, 3, 7, 40)

oBasicDoc:DoubleBox(nDown-3,8,nDown+1.5,92,1)

oBasicDoc:Font := oFont22CS

oBasicDoc:LineTextPrint(SELF:PelHead, nDown-2,15)

//------------ date and internal reference -------------

oBasicDoc:Font := oFont9CS

oBasicDoc:LineTextPrint(SELF:DateRef[1],nDown+4,75)

oBasicDoc:LineTextPrint(SELF:DateRef[2],nDown+5,75)

oBasicDoc:Font := oFont11CS

//------------------ the new patient -------------------

oBasicDoc:LineTextPrint(SELF:ToWhom, nDown+5,nacross)

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

oBasicDoc:MemoPlaceFJ(SELF:Para1, nDown+7, nAcross, 85)

oBasicDoc:MemoPlaceFJ(SELF:Para2, nDown+10, nAcross, 85)

oBasicDoc:MemoPlaceFJ(SELF:Para3, nDown+29, nAcross, 85)

oBasicDoc:MemoPlaceFJ(SELF:Para4, nDown+33, nAcross, 85)

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

//------------- order details, and to follow --------------

oBasicDoc:Font := oFont9CS

nAA := 3

oBasicDoc:BlockOfText(SELF:Items, nDown+15, nAA+7, 16)

oBasicDoc:BlockOfText(SELF:Descriptions, nDown+15, nAA+23, 38)

oBasicDoc:BlockOfText(SELF:Packs, nDown+15, nAA+61, 8)

oBasicDoc:BlockOfText(SELF:Qtys, nDown+15, nAA+73, 8)

oBasicDoc:BlockOfText(SELF:ToFs, nDown+15, nAA+81, 8)

oBasicDoc:Font := oFont11CS

//----------- sender details and signature ------------

oBasicDoc:BitMapPlace(SELF:SigFilePath, nDown+39, 10)

cText := SELF:Sender

oBasicDoc:LineTextPrint(cText, nDown+44,nacross)

cText := SELF:SenderTitle

oBasicDoc:LineTextPrint(cText, nDown+45,nacross)

//----------- letter type reference & if first or re-print ----------

cText := SELF:LetterRef

oBasicDoc:LineTextPrint(cText, nDown+45, 76)

//----------- director details at bottom left ---------------

oBasicDoc:Font := oFont8A

cText := SELF:Directors[1]

oBasicDoc:LineTextPrint(cText, nDown+47,nacross)

cText := SELF:Directors[2]

oBasicDoc:LineTextPrint(cText, nDown+48,nacross)

RETURN

END CLASS

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

METHOD DoubleBox(nDownT AS FLOAT, nAcrossT AS FLOAT, nDownB AS FLOAT, nAcrossB AS FLOAT, nPenWidth AS LONG) AS VOID

LOCAL r4Across1, r4Down1, r4Across2, r4Down2 AS FLOAT

LOCAL r4Across3, r4Down3, r4Across4, r4Down4 AS FLOAT

LOCAL oPoint1, oPoint2, oPoint3, oPoint4 AS Point

LOCAL oPelPen AS Pen

LOCAL oPelColor AS Color

LOCAL fPen AS Single

IF nAcrossB-nAcrossT <= 0

RETURN

ENDIF

IF nDownB-nDownT <= 0

RETURN

ENDIF

IF nPenWidth > 10

nPenWidth := 10

ENDIF

IF nPenWidth < 1

nPenWidth := 1

ENDIF

//------------ no Pen set ! --------------

oPelColor := Color.DarkViolet

oPelPen := Pen{oPelColor}

fPen := nPenWidth

oPelPen:Width := fPen

SELF:SingleBoxLineRealAcrossReal(nDownT, nAcrossT, nDownB, nAcrossB, oPelPen)

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

nDownT += 0.25

nAcrossT += 0.80

nDownB -= 0.25

nAcrossB -= 0.87

SELF:SingleBoxLineRealAcrossReal(nDownT, nAcrossT, nDownB, nAcrossB, oPelPen)

RETURN

METHOD SingleBox(oPen AS Pen, oPoint1 AS Point, oPoint2 AS Point, oPoint3 AS Point, oPoint4 AS Point) AS VOID

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

SELF:g:DrawLine(oPen, oPoint2, oPoint3)

SELF:g:DrawLine(oPen, oPoint3, oPoint4)

SELF:g:DrawLine(oPen, oPoint4, oPoint1)

RETURN

METHOD SingleBoxLineRealAcrossReal(nTLine AS FLOAT, nTAcross AS FLOAT, nBLine AS FLOAT, nBAcross AS FLOAT, oPen AS Pen) AS VOID

LOCAL oPointTL AS Point

LOCAL oPointTR AS Point

LOCAL oPointBL AS Point

LOCAL oPointBR AS Point

oPointTL := SELF:LineRealAcrossReal2Point(nTLine,nTAcross)

oPointTR := SELF:LineRealAcrossReal2Point(nTLine,nBAcross)

oPointBL := SELF:LineRealAcrossReal2Point(nBLine,nTAcross)

oPointBR := SELF:LineRealAcrossReal2Point(nBLine,nBAcross)

SELF:SingleBox(oPen, oPointTL, oPointBL, oPointBR, oPointTR)

RETURN

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


Laser labels

Producing Your Own Laser Labels :

With the existing methods in the BasicDocument class we can produce laser labels as shown below.

All we need to do is handle the supply of data from data file, and control the 'pointer' which positions the label lines on the page.

This is not hard to do but takes a bit of time and testing to get things right - top tip - test with paper and not labels in the printer :-)

If you want to know more about this look for 'ClickPrintVDN' in the new year, or go to DevFest and get a free copy of this and final release 'ClickStartVDN' on CD.

PHH December 2007.


Report Proportional Font

Producing Quality Looking Reports :

Once we decide we would like our company internal reports to look nice, by using proportionally spaced fonts in place of those limited fixed space ones, we enter a different world altogether.

We can now no longer print full lines, as we need the columns to line up. And, with currency and some other decimal numbers we need the 'point' to be lined up - as shown in the example below.

In fact I realised after reading Alwyn's VOcab notes that what I needed was to have my data stored in file and a user interactive screen for changing format and layout just before printing.

Yes, there could always be a default to allow user's an easy print out facility, but it would be nice to be able to control all the rest. Even select which columns are printed, and their order.

As you can see below, the sample print out is not yet 100% okay as the heading are not quite lined up correctly. This is simply because I discovered event handling in dotNet and did not get back to finish things - yet. It will be okay in ClickPrintVDN however.

Now that we can produce a proportionally spaced report output format we can think of an interactive screen to have fun with what we can do.

The one below is a demo and test app which does many of the things we would wish, but not all at the moment.

Ideally, I would like to store and use both the column selection, and the order, in the printed report. I am not far from that so look out for ClickPrintVDN.

What we do have working is the possibility to change the orientation, as well as the number of lines per page, the font style and size. Obviously after such changes the data needs to be re paginated, as the page number is kept as a data item in the file along with the other row details.

It is NOT a good idea to print on the fly - what is going to be placed on which page, needs to be sorted out BEFORE the act of printing. This both makes coding simpler and more straight forward, but also allows for some more useful features to be included - like keeping lines together, and not having orphans and widows etc. etc.

With this approach it is not necessary to trial print your document to find out how many pages it with take up - you already know before hand.

Below is another sample print out of the same page but after orientation has been altered (now in portrait), as well as font style and size, and lines per page too.

If you consult ClickPrintVDN in the future you will also see how I have provided the facility for allowing, or preventing, one column to over-print another.

Since the columns are printed separately then there is nothing to stop them printing over content from another column, unless you, the developer, provides the necessary control.

I think this feels rather neat as it means the font size can be increased a lot if necessary but the overall readability may be maintained.

PHH December 2007.


Multiple Pages

Controlling Output to Page :

Once you have battled with printable page size on your printer, and which way the numbers apply to positions / coordinates on the printable page, you then need to be able to skip from one page to another when it is necessary.

No, you can't skip back pages and print a bit more on an earlier page - well, not to my knowledge. But, I would like the facility :-) Any takers ?

Below is what I got in some early tests when all I had to work with was some early DevFest notes from Chris Pyrgas. Thanks Chris, without these, the BasicDocument class would not exist.

I have to admit though, the more than one page problem was one I had to fight myself, and with the help of a few 'tomes' on C# for .NET in hand.

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

Unfortunately, these tomes were not a lot of use. So after a bit of fiddling, and a slice or two of good luck, I managed to get three pages which were not identical. And were my test equivalent of having printed three separate pages.

As you can see, not a lot of use but the ideas were starting to be put in place.

Now I have built the page count and control into the BasicDocument class. But, as said earlier, you will have to wait for that, or got to DevFest 2008 for a fun time out !

I have recently done some work on multi page reports, running into hundreds of pages - this will be available in ClickStartVDN in the New Year (2008).

PHH December 2007.


The Trick

Although I will show in full detail, and fully explain, in the ClickPrintVDN help notes file, I will list here what is required in order to get multiple pages to print / work !

Any code wishing to print multiple pages will need a method similar to this. This method is simply triggered by a push button GUI click to Print Pages.

The "thing" to realise is that the printing process is actually caused, or kicked into action, by a call to either a PrintPreview "ShowDialog" or a "Print" for my BasicDocument class - which is a sub-class of the PrintDocument class.

So the event which uses the "PagePrintEvent" handler, in this case "PrintSinglePageHandler()" happens when we execute either of these two code lines.

I like to have a check box on the Form which allows direct printing to the printer, or a preview screen to be shown. This is illustrated in the same code section below.

METHOD PrintSelected() AS VOID

//------------- print page all pages -------------

SELF:oBD:LinesPerPage := 66

SELF:oBD:SetPageBoundaries()

SELF:oBD:PrintPage += PrintPageEventHandler{SELF , @PrintSinglePageHandler()}

IF SELF:oBD:PrinterSettings:IsValid

//MessageBox.Show("Valid OK")

ELSE

//MessageBox.Show("Not Valid")

ENDIF

//------------ set up for page range ------------

SELF:PageToPrint := SELF:PageFirst

IF ocbPreviewDialogScreen:Checked

SELF:oPreviewDlg:=PrintPreviewDialog{}

SELF:oPreviewDlg:Document := SELF:oBD

SELF:oPreviewDlg:ShowDialog()

ELSE

SELF:oBD:Print()

ENDIF

SELF:oBD:PrintPage -= PrintPageEventHandler{SELF , @PrintSinglePageHandler()}

SELF:oBD:Dispose()

RETURN

It now has to be realised that the event handler method has to finish with a statement to set that either there is or there isn't any more to print. Look at the following code :-

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

LOCAL oGraphics AS Graphics

LOCAL cPage AS STRING

LOCAL cPageNum, cPageline AS STRING

LOCAL nLen, nRest AS DWORD

LOCAL aPage AS STRING[]

oGraphics := e:Graphics

SELF:oBD:SetGraphics(oGraphics)

cPageNum := "[Page : "+SELF:PageToPrint:ToString()+" of "+SELF:TotalPages:ToString()+"]"

nLen := cPageNum:Length

nRest := SELF:LineLength-nlen

cPageLine := SPACE(nRest)+cPageNum

cPage := SELF:Header+aPages[SELF:PageToPrint]:ToString()+SELF:Footer+cPageline+CRLF

aPage := SELF:PageToLines(cPage)

SELF:PageDump(aPage, SELF:oBD)

IF SELF:PageToPrint = SELF:PageLast

e:HasMorePages := FALSE

ELSE

SELF:pageToPrint +=1

e:HasMorePages := TRUE

ENDIF

RETURN

The body of the code has to determine what is printed on the page canvas, and also at the end to say whether there is more to come.

The two lines involving the graphics are essential, as are the lines to do with more pages. However the lines set back a tab, are the page specific ones which will change from example to example.

If this is all too much for you, you will need to study ClickPrintVDN in the spring of 2008.

PHH December 2007.


VO to Vulcan - a way forward

My Solution to Moving Forward :

I would like to share with you my carefully thought out way of getting into Vulcan while still maintaining major VO network apps.

One area in which my clients are well aware that there is potential for many problems, is the area of printer control and printing.

They are aware that my VO solution, although successful, is build on third party code kindly donated to me by a man in Holland. This code is quite sizeable and I am not able to maintain it. It also lacks full control over printer 'bins' or 'trays', and probably a few other things too.

This is my way forward :

I use the power and ease of availability of the dotNet classes and methods, to access and change the printer settings at a much higher level.

At the moment if a printer driver has not be configured correctly (classic e.g. is Letter size for A4) then we can have problems in the office - folk needing to go to the printer to press buttons etc. etc.

With dotNet we could force a 'printer-settings' properties 'set' after instantiation.

OK - what do I do about this after demonstrating to the client that dotNet is what we want.

My solution is to write a full printing suite which will do all required on the printing and printers side of things. All my VO apps will do is gather and store the data, and then call the Vulcan app with a command line parameter or two.

This 'passed' information will point to a config file which has been made in VO and placed in the necessary folder with the required filename.

The VDN app will run modal and pass back exit information to the VO launching app via the clipboard and/or a 'HandShake' text file.

This way VO will know exactly what happened at print time. The demo trial tests show that this appears to be seamless and quite possible.

Because I will need to have Vulcan support (run time) files on the network PCs for this, they will be there for use when other 'customer modules' need to be created and added to the system.

There is no way I can consider (yet) to re-write the VO application with its over 170 separate GUI screens. But at least I am into .NET programming from now!

I think I will wait on the GUI converting side of things, as WPF may well be the easiest way to go forward - with its very clean interface between the declarative interactive screen and the code and event handlers behind it.

Doing some work getting to know and use WinForms seems a good way to learn event handling prior to WPF however.

Note the code for most of this linking is available in these notes, however, I may provide a 'ClickLinkVDN' set of dedicated notes when I solve my problem above.

The forms above are what at present make up a Vulcan module which is being developed to to be linked with VO to do all the printing. The VDN app will be launched from within VO and provide reporting and letter printing facilities, as well as label printing and a printers tool.

Look out for more on this in ClickPrintVDN in the first part of 2008.

The site to look at is www.ClickStartVulcan.net and it has just be acquired in December 2007 - so lots needs to be done.

PHH December 2007.


Section 4 - Printers

Finding the Windows default printer :-

( Contribution from Colin Newell - thanks Colin! )

LOCAL oPS AS PrinterSettings

oPS := PrinterSettings{}

MessageBox.Show(oPS.PrinterName, "Default Printer :- ")

When an instance of the PrinterSettings class is created all the default settings are there to be used, before any changes are made.

See below for this in action.

Now go and look at other settings for printers, and even look out the ExaminePrinters app - a useful tool for checking out printers on any PC as well as network stations.

This is available as a download from the ClickStartVulcan.net site of mine.

I will include the PRG file and the project export - or application export - file.

It is also a handy tool for finding out which of all the Windows printers available, are not set to A4, and may be causing problems related to this.

PHH December 2007.


Getting all Windows Printers

It is often very useful to be able to present the GUI user with a list of the available Windows printers.

The code shown below gets the available printers from the operating system and places their names into a Combo Box control.

The Combo Box object is named "cbWinPrinters".

METHOD FillWinPrinters() AS VOID

LOCAL acPrinters AS STRING[]

LOCAL oPSSC AS PrinterSettings.StringCollection

oPSSC := PrinterSettings.InstalledPrinters

acPrinters := STRING[]{oPSSC:Count}

oPSSC:CopyTo(acPrinters,0)

SELF:ocbWinPrinters:Items:Clear()

SELF:ocbWinPrinters:Items:AddRange(acPrinters)

SELF:ocbWinPrinters:SelectedIndex := 0

RETURN

Now there is an alternative way of accessing the individual printer names which looks a little more familiar to VO users.

nCount := PrinterSettings.InstalledPrinters:Count

cText += "Total number of available printers :- "+NTrim(nCount)+cR2

SELF:ocbWinPrinters:Items:Clear()

FOR i := 0 UPTO nCount-1

cPrinter := PrinterSettings.InstalledPrinters[i]

cText += SPACE(4)+"{ "+cPrinter+" }"+cR

AADD(aPrinters, cPrinter)

NEXT

cText += "======================================="+cR2

These individual printer names could have been added to the combo box one at a time using this code structure. Look at Items:Add() for this control.

( See section 22 - sub-section on combo boxes. )


Finding Printer Settings

Since we can now gain easy access to any of our Windows printers - ( see previous sub-section in Section 4 ) - we now need to know how to access the settings for a chosen printer.

Here are a few pointers to get you going :- (note the more familiar way to get individual win printer names at the very bottom.)

LOCAL oPS AS PrinterSettings

oPS := PrinterSettings{}

cMyName := cSelectedPrinter

oPS:PrinterName := cMyName

cText += "Printer in Focus :- "+cMyName+cR2

IF oPS:IsValid

nAngle := oPS:LandscapeAngle

aPageSettings := oPS:DefaultPageSettings

cText += "Duplex printing capability : "+oPS:Duplex:ToString()+cR

cText += "Collate pages : "+oPS:Collate:ToString()+cR

lMyLandscape := aPageSettings:Landscape

oMySize := aPageSettings:PaperSize

//oMySource := aPageSettings:PaperSource

oMyMargins := aPageSettings:Margins

oMyArea := aPageSettings:PrintableArea

oMyResolution := aPageSettings:PrinterResolution

oMyBounds := aPageSettings:Bounds

lMyColor := aPageSettings:Color

HardX := aPageSettings:HardMarginX

HardY := aPageSettings:HardMarginY

cText += "Landscape : "+lMyLandscape:ToString()+cR

cText += "In colour : "+lMyColor:ToString()+cR2

cText += "Margins :- "+cR

cText += SPACE(5)+"Left : "+oMyMargins:LEFT:ToString()+cR

cText += SPACE(5)+"Right : "+oMyMargins:RIGHT:ToString()+cR

cText += SPACE(5)+"Top : "+oMyMargins:Top:ToString()+cR

cText += SPACE(5)+"Bottom : "+oMyMargins:Bottom:ToString()+cR2

cText += "Hard Margins :- "+cR

cText += SPACE(5)+"Left : "+HardX:ToString()+cR

cText += SPACE(5)+"Bottom : "+HardY:ToString()+cR2

cText += "Printable Area : "+cR

cText += SPACE(5)+"Left : "+STR(oMyArea:LEFT,8,2)+cR

cText += SPACE(5)+"Right : "+STR(oMyArea:RIGHT,8,2)+cR

cText += SPACE(5)+"Top : "+STR(oMyArea:Top,8,2)+cR

cText += SPACE(5)+"Bottom : "+STR(oMyArea:Bottom,8,2)+cR2

oMySourceCollection := oPS:PaperSources

cText += "Paper Source(s) : "

nCount := oMySourceCollection:count

cText += " [ Number : "+NTrim(nCount)+" ]"+cR2

FOR i := 0 UPTO nCount -1

cText += SPACE(4)+oMySourceCollection:Item[i]:ToString()+cR

NEXT // i

cText += cR

cText += "My first printer is ... "+oPS:PrinterName+cR2

cText += "Maximum copies :- "+NTrim(oPS:MaximumCopies)+cR2

cText += "Landscape angle :- "+NTrim(nAngle)+cR2

ENDIF

_wait(cText)

RETURN

Below is a useful variation to the method in "Getting WinPrinter", and is probably more familiar to VO developers coming over to Vulcan.

//=========== this produces the printers list at the top of image ======

nCount := PrinterSettings.InstalledPrinters:Count

cText += "Total number of available printers :- "+NTrim(nCount)+cR2

FOR i := 0 UPTO nCount-1

cPrinter := PrinterSettings.InstalledPrinters[i]

cText += SPACE(4)+"{ "+cPrinter+" }"+cR

AADD(aPrinters, cPrinter)

NEXT

cText += "======================================="+cR2

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

RETURN
=============================================================================================================================

I have used and tested the code above, but would like to know if anyone has used different trays or "bins" on printers using these "Printer Settings".

Please post a message or short article if you can inform others on how to access the trays/bins and switch output accordingly.


Section 5 - Sound & Music

It is quite easy to improve your applications by the use of sound and music from .WAV files.

The namespace to use is "Microsoft.VisualBasic" and the class "Devices.Audio".

To play a music track converted from CD to a .WAV file by suitable easily available software, do as follows :-

USING Microsoft.VisualBasic
................
................
LOCAL oAudio AS Devices.Audio
oAudio := Devices.Audio{}
oAudio:Play("c:\vdn_test\track13.wav")
................
oAudio:Stop()

It is possible to select a "play mode" using one particular overload method of ":Play()" and you may choose from background, background looping, or waiting to complete.

USING Microsoft.VisualBasic
................
................
LOCAL oAudio AS Devices.Audio
LOCAL oAPM AS AudioPlayMode
                 //oAPM := AudioPlayMode.Background
                 //oAPM := AudioPlayMode.WaitToComplete
oAPM := AudioPlayMode.BackgroundLoop
oAudio := Devices.Audio{}
oAudio:Play("c:\vdn_test\track13.wav", oAPM)

As well as playing the user selected .WAV file you may ask the system to play one of its "standard" sounds as follows :-

oAudio:PlaySystemSound(System.Media.SystemSounds.Hand)

                 //oAudio:PlaySystemSound(System.Media.SystemSounds.Beep)

Other enumerations of SystemSounds are :-

.Asterisk
.Exclamation
.Question

This does work, but I suspect it may work more correctly in XP than Vista. It did not give me quite what I was expecting, however it does produce a suitable sound.

You will need to check it out for yourself and your version of Windows.

I also suggest referring to the Framework 2.0 SDK documentation for more information on this Audio class.

One idea for a project is to use Windows Explorer to search your PC for all the .WAV files. When you have them, select all and copy them to a working folder.

Write an app. to find three .WAV file categories - very short, medium, and long. Put these into list boxes of some sort, and when the user selects a file from the list box play it using the code above.

As an extension to this you may like to display an image from file onto a picture box control. You would need to co-ordinate the images with the file name.

Look at Wiki articles on displaying images from file, as well as copying audio files to the clipboard.


Section 6 - GUI Event Handling

You will be pleased to know that event handling is very straight forward for Windows Forms and is done at a control level.

For those coming from VO where event handling was much more at a form level, this should be a breath of fresh air.

I have found myself getting into places I would not dare think of in the past ! And, it is fun :-)

Once you become accustomed to the syntax of the event handler code lines you will be away.

It took me a while to realise that I could let VIDE properties box, and then intellisense, take all the hard work and memory demands away, to leave the fun bit.

I will explain as we go how we may take the hard work out of this exciting part of GUI coding.

If I can do what I have done in my development demo apps, without any helpnotes like this, then what will you do when I take the hard work away and get you to the fun part very quickly ?

Good Luck with your eventing.


Level (i)

Once you get into the swing of using event handlers, it seems quite easy in .NET to "Set", "Clear", and "Re-set" event handlers to suit the needs of your application.

Each control on the Form has a rich set of events which can be selected to trigger user supplied methods. These methods can be termed "handlers" as they handle what needs to be done in response to the event taking place.

The Form itself has a list of such events too.

The user writes code to add into the "stub" of such methods. The "stub" is usually provided by the IDE in some automatic fashion.

By letting the IDE provide the "stub" the correct syntax is applied for the control in focus, without the developer needing to remember all the necessary details (arguments and such like).

Let us look at what happens when we wish to have a Push Button cause something to happen when clicked.

Let us say we have a button on the Form called "pbPopulateGrid"

When we make selections in the IDE to have a "click" event for this button, a "stub" method is created for the developer similar to that below :-

METHOD pbPopulateGridButtonClick(oControl AS OBJECT , e AS System.EventArgs) AS VOID

//----

//---- stub provided by the IDE ----

//----

RETURN

It is also quite likely that the IDE also puts a line in the "inisializeform()" section of the code similar to below :-

SELF:opbPopulateGrid:Click += EventHandler{ SELF , @pbPopulateGridButtonClick() }

This is simply specifying the event handler for the event of a click on the button.

The last part "@pbPopulateGridButtonClick()" links to the name of the method to be used for that event. You may choose this name in the IDE usualy, but the example here is the default supplied by my specific IDE, which is Chris's VIDE.

Notice the "+=" just before EventHandler{ ...} this is to "set" the handler, or put it on. It is also possible to remove the same handler using "-=".

So if later in the code relating to the Form, the developer puts the line of code :-

SELF:opbPopulateGrid:Click -= EventHandler{ SELF , @pbPopulateGridButtonClick() }

Then the "Populate Grid" button would no longer respond to a click - we would say it had stopped working !

However, it is possible to "set" more than one event handler and this also implies the same one more than once. You need to be very careful when you start to do "fancy" things with event handlers as things can easily happen which you did not intend :-) Just yesterday I launched an external application twice because I had applied the event handler code line twice, in different places. Quite a surprise to do two data backups at once!

For this reason I prefer to have all my event handlers set by my own code, and not any at all in the "inisializeform()" section.

There is a "trick" to use in some IDEs, and that is to use the Properties-Events tab to provide the "stub" and then to use it again with a blank entry, to get it to remove the line from the "inisializeform()" section. Before removing this line of code I copy and paste it into my own code section.

This way you get the system to provide the correct syntax for method and setting handler, and yet you have full control over when you apply the handler code itself.

I often have a method for the Form called "SetMyEventHandlers()" which I call from near the end of the CONSTRUCTOR(). In this method I set all the event handlers for the controls and the Form itself.

This way it is easy at a glance to see what is set and what is not.

Finally, for this simple introduction to Event Handling, I would suggest that this approach can easily be taken further, and can be applied to much more complex situations than a push button click event.

Note that it would appear that you can attempt to remove an event handler with "-=" even when it is not set, and cause no apparent "damage".

For more event handling information see articles "Event Handling Going Further" & "Event Handling Getting Back Information". (Sub-sections of this chapter.)

PHH: Oct. 2007


Level (ii)

Hopefully, you have read and studied the first article on event handling. This showed how to use an organised approach to setting a handler for the click event of a push button.

It is no harder to set a handler for a "radio button" click event, or a "check box" click either. If the radio button is called "rbProfAdv" and the check box called "cbToolTips" then these would have code similar to :-

SELF:orbProfAdv:Click += EventHandler{ SELF , @rbProfAdvClick() }

METHOD rbProfAdvClick(oControl AS OBJECT, e AS System.EventArgs) AS VOID

//----

//---- stub provided by IDE ----

//----

RETURN

SELF:ocbToolTips:Click +=EventHandler{ SELF , @cbToolTipsClick() }

METHOD cbToolTipsClick(o AS OBJECT, e AS System.EventArgs) AS VOID

//----

//---- stub provided by IDE ----

//----

RETURN

I prefer to only include in the "stub" a call to another method, which at least I can also call myself from other parts of my own code, when I choose. So it may look like the following for this "action" method of the "ToolTips" click.

METHOD cbToolTipsClick(o AS OBJECT, e AS System.EventArgs) AS VOID

SELF:ShowToolTipOptions()

RETURN

So if I wished I could also use the "action" method from a menu bar selection.

Now we can move on from "clicking" to see that the event for a radio button of having focus is very similar, and certainly can be dealt with in the same way as previously.

SELF:orbPersonal:GotFocus += EventHandler{ SELF , @rbPersonalGotFocus() }

METHOD rbPersonalGotFocus(oControl AS OBJECT, e AS System.EventArgs) AS VOID

//----

//---- stub provided by IDE ----

//----

RETURN

All these events up till now have used "System.EventsArgs" for "EventHandler" but there are others we need to know about. Lets look at what we get when we need to trap a mouse double click within a list box boundary.

SELF:olbColsSelected:MouseDoubleClick +=MouseEventHandler{ SELF, @ColumnSelectedMouseDoubleClick() }

METHOD ColumnSelectedMouseDoubleClick(o AS OBJECT, e AS MouseEventArgs) AS VOID

//----

//---- stub provided by IDE ----

//----

RETURN

This looks complicated just simply because of the long names, but the structure and form is similar to what we have already seen. Only now we have a "MouseEventHandler" and "MouseEventArgs".

While we are still doing simple things we don't need to know about the properties, or methods, of "e" the event passed, we just write code to fill the "stub" or and action method to call from the "stub".

Here is another "cool" trick. If you have intellisense then it will inform you of what the options are, just type "e:" in the "stub" and when the colon appears you will be given a list of possibilities by the IDE. This is handy when you need to find out which mouse button has been pressed and so on. Look at further levels of article on Event Handling for this sort of information.

For now, have some fun, just start to use what is above, and also a final new event for this section, the "Selection Changed Committed" event for a combo box. This allows you to select an item from the drop down list.

SELF:ocbWinPrinters:SelectionChangeCommitted += System.EventHandler{ SELF, @WinPrintersSelect() }

METHOD cbWinPrintersSelectionChangeCommitted(o AS OBJECT, e AS EventArgs) AS VOID

//----

//---- stub provided by IDE ----

//----

RETURN

A line of code similar to that below will allow you to use the name of the printer selected from the drop dowm list.

cPrinter := SELF:ocbWinPrinters:SelectedItem:ToString()

Enjoy your "eventing" and when you are happy with what has been done so far, look at the next article: "Event Handling Getting Back Information". (Sub-section four in this chapter.)

PHH: Oct.2007


Level (iii)

So far in articles "Level One" and "Level Two" we have seen how to respond to GUI events for push buttons, radio buttons, check boxes, and drop down lists (combo boxes). We also saw how to respond to a double click of the mouse within the boundaries of a list box.

However, in all of these examples we simply reacted to the event detected by the system. We did not get any information "back" from the EventArgs.

Let us now look at an example of a "Mouse Down" event after which we wish to have our event handling method (or action method) respond differently to a left button click than a right button click. Or even for that matter, the scroll wheel input.

The code to set the event handler and the "stub" method to handle the event are similar to what we have seen previously and are shown below :- (this "Mouse Down" event is for a List Box control but others are similar)

SELF:olbColsSelected:MouseDown += MouseEventHandler{ SELF,@ColumnSelectedBothWaysClick() }

METHOD ColumnSelectedBothWaysClick(o AS OBJECT, e AS MouseEventArgs) AS VOID

//----stub from IDE ----

//----

//---- we need to add lines to detect which button was used ----

RETURN

In order to have our code react accordingly, we need information supplied to inform about the type of mouse event which has taken place. For this information we use the following :-

IF e:Button = MouseButtons.left

//---- reaction to a left click -----

ENDIF

We could have used other enumerations for MouseButtons:-

MouseButtons.Right

MouseButtons.Middle

MouseButtons.None

MouseButtons.XButton1

MouseButtons.XButton2

So lets see what the final event handler may look like if we are trying to sort the list box in order A-Z for a left mouse button click, and Z-A for a right button click.

METHOD ColumnSelectedBothWaysClick(o AS OBJECT, e AS MouseEventArgs) AS VOID

IF e:Button = MouseButtons.left

SELF:DoSortAZ()

ENDIF

IF e:Button = MouseButtons.right

SELF:DoSortZA()

ENDIF

RETURN

Before we leave this small section on getting back information from "EventArgs" let us look at the "Cell Painting" event for a DataGridView control :-

SELF:oWarpGridView:CellPainting += DataGridViewCellPaintingEventHandler{ SELF, @WarpGridViewCellPainting() }

METHOD WarpGridViewCellPainting( sender AS OBJECT, e AS DataGridViewCellPaintingEventArgs ) AS VOID

rect := e:CellBounds

nRow := e:RowIndex

nCol := e:ColumnIndex

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

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

RETURN

As you can see the information given back after the event is a rectangle and two numeric values representing the number of rows and columns. Although it takes a bit more work to do something with is information, at least we can see that is is passed back to be used. We will look at how we may get a colourful grid display in another article on the DataGridView control.

Finally, for now, let us see how a simpler event for the DataGridView may be of good use to us.

Let us see how we can pick up the double click of the mouse in a cell of the DataGridView (DGV) control and use this to know which cell was clicked. Below is the code generated by the IDE with some useful code to pick up from the DGV itself the information we need :-

SELF:oWarpGridView:CellDoubleClick += DataGridViewCellEventHandler{ SELF, @WarpGridViewCellDoubleClick() }

METHOD WarpGridViewCellDoubleClick(oControl AS OBJECT , e AS DataGridViewCellEventArgs) AS VOID

LOCAL oDGVC AS DataGridViewCell

LOCAL oContent AS OBJECT

oDGVC := oWarpGridView:CurrentCell

nRow := oDGVC:RowIndex

nCol := oDGVC:ColumnIndex

oDGVC := oWarpGridView:Item["patunref",nRow]

oContent := oDGVC:Value

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

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

RETURN

In the next article on Event Handling we will look at how to pick up from a mouse click in an image, which part of the picture has been clicked and therefore what to do in response. This will entail using the coordinates given back via the EventArgs.

Have fun with eventing and see you in the next article - "Event Handling In Control" (Level Four/Five)

PHH: Oct.2007


Level (iv)

Back to the problem of clicking an image (stretched across a PictureBox control) and finding out exactly where the mouse pointer is within the bounds of the control.

You may be familiar with the attached image from an earlier "Pearls" VOPS article. It shows a PictureBox control with an image (from file on disk) stretched to fit the customized size of the control. The image is of five desktop icons in a horizontal row.

By the nature of my image, there are five equal areas which can be associated with the icon shown within the image itself. What I wanted to do was simply associate a mouse click with one of the five areas, and hence the icon in the clicked area. A bit like clicking icons on the Desktop.

To be able to make such an association, first I needed to find the coordinates of the mouse pointer when the click event happens. Then I can use some simple mathematics to work out which of the areas has been clicked, by finding the dimensions of the PictureBox itself.

I have set my demo so that there is a MessageBox displayed to show the relevant information, when the mouse is clicked.

Let us see how we can first detect the mouse click event, and then handle it appropriately. The PictureBox control is called "opbFiveIcons" in my example, and below is the section from the InitializeForm() method created by the IDE. Note I have left the setting for the event handler in the InitializeForm section, against my normal practice where I like to gather all the event handler settings into one custom method called "SetMyEventHandlers()". This just proves I am human :-)

SELF:opbFiveIcons := PictureBox{}

SELF:opbFiveIcons:Name := "pbFiveIcons"

SELF:opbFiveIcons:Location := Point{ 0 , 160 }

SELF:opbFiveIcons:Size := Size{ 360 , 96 }

SELF:opbFiveIcons:TabIndex := 11

SELF:opbFiveIcons:MouseDown += MouseEventHandler{ SELF , @pbFiveIconsMouseDown() }

SELF:Controls:Add(SELF:opbFiveIcons)

Now we need to have a few lines at the end of the Constructor() method which will place the image from disk file, onto the PictureBox control and stretch it to fit exactly.

//------------------ put icons JPG on the form --------------------

LOCAL oImage AS Image

oImage := System.Drawing.Image.FromFile("c:\vdnuk\fiveicons.jpg")

SELF:opbFiveIcons:Image := oImage

SELF:opbFiveIcons:SizeMode := PictureBoxSizeMode.StretchImage

I realize that there is an easier way to code this as follows :-

SELF:opbFiveIcons:Image := System.Drawing.Image.FromFile("c:\vdnuk\fiveicons.jpg")

But at the time in my code I was also making the image available on the clipboard, so I did it the first way so I could use :-

Clipboard.Clear()

Clipboard.SetImage(oImage)

to simply provide this same image for another running app to pick up !

Anyway, enough of that clipboard stuff, back to our mouse clicking. Below is the event handler, which uses information supplied by the "MouseEventArgs" object. Remember the joys of intellisense and refer to the other "Pearls" image. For this example, when we are typing in our code, all we have to do to get a list of the available properties and methods relating to "e" is to type "e:" and as soon as the colon appears, the list box is displayed as shown in the reference.

You will see in the code below that we have used some of these properties to good effect.

METHOD pbFiveIconsMouseDown(o AS OBJECT , e AS MouseEventArgs) AS VOID

LOCAL nX, nY, nPictureWidth, nPW, nBin, nBinW AS LONG

LOCAL oLocation, oPictureSize AS Point

LOCAL cText, cBin AS STRING

nPictureWidth := SELF:opbFiveIcons:Size:Width

nPW := nPictureWidth

nX := e:X

nY := e:Y

oLocation := e:location

nBinW := INTEGER(nPW/5)+1

nBin := INTEGER(nX/nBinW)+1

DO CASE

CASE nBin = 1

cBin := "Vulcan Printing"

CASE nBin = 2

cBin := "Slide Show"

CASE nBin = 3

cBin := "BEST Project 1"

CASE nBin = 4

cBin := "First DataView"

CASE nBin = 5

cBin := "Install VOUGcd"

ENDCASE

cText := "Mouse pointer coordinates when clicked in Icons image :-"+CRLF+CRLF

cText += "X value :-"+NTrim(nX)+CRLF

cText += "Y value :-"+NTrim(nY)+CRLF

cText += "Picture bin width :- "+NTrim(nBinW)+CRLF

cText += "In Picture bin :- "+NTrim(nBin)+CRLF

cText += "Icon Picture is :- "+cBin+CRLF

MessageBox.Show(cText)

RETURN

I have named the five areas of the image "bins", so we have bins 1 to 5. I thought it may be a bit confusing to call them "icons" when they weren't really. Also notice that we are only interested in the width of the PictureBox, as the overall width splits into five equal parts. The height of the control is of no interest to us while solving this problem.

Also it is important to note that the information on the X & Y of the click, is given in coordinates related to the PictureBox control, and not the Form, or the screen. This seems obvious when you think about it, as mouse click is a control related event.

What I intended to do, and never got round to it, was to finally link the event to launching the correct application for the icon which is shown in the "bin" area. I will leave you to do that as you can see how to do this from articles in Wiki on launching external apps. And one of your other apps is just another external :-)

Tip to do above - look out the "Process" class in Framework 2.0 SDK for the relevant info, as well as in Wiki and "Pearls" articles.

Hope this provides you with some fun !

PHH Nov.2007

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

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


Level(v)

Hi Guys and Dolls,

I thought I had better try and raise the "tone" a notch after the "Karl & Don show", crying over their curly braces :-)

Just to let you know I discovered the "PictureBox" class while I was doing some demo "stuff" for the VOUG meeting.

I had previously put images on some of my VO screens to brighten things up. Especially my Primary School Library software - see attached image of the review screen.

However, I was always disappointed how limited in VO I was to anything like real control over using and displaying images from file. Including putting signatures on my printed letters. I could only do this by getting a class from Michael Rubinstein (thanks!).

Now things are different, and it is much easier for the standard guy to do a lot more with images in .NET so you will have to give it a go !

Within a short time I was not just displaying a file image, and over the full PictureBox area (easily!), but I was also clicking with the mouse and changing the image too. Really refreshing stuff, if not that useful to some of our community !

I think the images attached show enough of the code for you to try it for yourself.

First you set the event handler (for the picture box object called "SlideShow") as below :-

//-------------- picture box - Image change ----------------

SELF:opbSlideShow:Click += EventHandler{ SELF , @pbSlideShowClick() }

Then the event handler method - which I like to keep free of the actual "action" code, and point to a standard "action" method :-

METHOD pbSlideShowClick(o AS OBJECT , e AS System.EventArgs) AS VOID

SELF:ShowPictureImage()

RETURN

Then we need the "action" METHOD itself :-

METHOD ShowPictureImage() AS VOID

SELF:opbSlideShow:SizeMode := PictureBoxSizeMode.StretchImage

DO CASE

CASE SELF:PicturePointer = 1

SELF:opbSlideShow:Image :=

System.Drawing.Image.FromFile("c:\voug_cd\vdnuk\qc.bmp")

CASE SELF:PicturePointer = 2

SELF:opbSlideShow:Image :=

System.Drawing.Image.FromFile("c:\voug_cd\vdnuk\greatbearlogo.jpg")

CASE SELF:PicturePointer = 3

SELF:opbSlideShow:Image :=

System.Drawing.Image.FromFile("c:\voug_cd\vdnuk\PelLogo1b.bmp")

CASE SELF:PicturePointer = 4

SELF:opbSlideShow:Image :=

System.Drawing.Image.FromFile("c:\voug_cd\vdnuk\imag0488.jpg")

OTHERWISE

ENDCASE

SELF:PicturePointer +=1

IF SELF:PicturePointer > 4

SELF:PicturePointer := 1

ENDIF

RETURN

All I am doing here is keeping a pointer which tracks which of the four images is being displayed and when necessary causes a "cycle", or looping to occur.

If you really want to be "swish", have a look at the Audio article in Wiki and get some music to play in the background - different music for different pictures.

That seems like a good idea for the London VOUG meeting - will have to remember to put in the speakers however - and cables :-)

Have a look in Wiki as I will document this a little better, and also show how you may pick up mouse clicks on one image and turn these into icon numbers - so effectively pointing at different parts of the image - possibly to start a new app to launch. Image is also attached.

Hope this helps.

PHH Oct.2007

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

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

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


Section 7 - Using the Clipboard

In order to get the most of the flexibility of Drag and Drop it will be necessary to be able to use the Windows clipboard to store and retrieve data.

The Clipboard class in .NET provides some easy to use tools (Static methods) for some standard data types. These are Text, Audio, and Images.

I will use the words "push" and "pull" for the actions of putting data into the clipboard and getting it out for use elsewhere.

An example of putting some text into the clipboard is shown below.

LOCAL cPush AS STRING

Clipboard.Clear()

cPush := "our first PUSH test to clipboard"

Clipboard.SetText(cPush, TextDataFormat.TEXT)

Once the text data from this example is in the clipboard it may be used by any other application running at the time.

In order to "pull" back the stored data we can do as follows :-

LOCAL cPull AS STRING

IF Clipboard.ContainsText(TextDataFormat.TEXT)

cPull := Clipboard.GetText(TextDataFormat.TEXT)

otbPullFromClipboard:TEXT := cPull //....... a WinForms Text Box control .......

ELSE

otbPullFromClipboard:TEXT := "can't find text in clipboard"

ENDIF

Notice that it is necessary to say which text format you are using.

There are a few to choose from. The TextDatFormat enumerations are :-

TextDataFormat.TEXT

TextDataFormat.Rtf

TextDataFormat.Html

TextDataFormat.CommaSeparatedValue //..... known as CSV .....

TextDataFormat.UnicodeText

For a small 'fun' project create, or adapt, two demo applications, one to PUSH and one to PULL.

I put a text box control on app1's Form which allowed me to type in text, then click a push button to send the input text to the clipboard.

On the Form for app2 I put a push button to PULL the clipboard data and then display it in another text box control.

It is fun to see data moving from one app to another, especially if you have two monitors with one app on one and the other app on the second monitor.

We will see how we can improve these apps to move image and audio data too.

You need to remember that the clipboard is a "general" facility and used by many things.

If you do a "print screen" part way through your demo/test you will lose your data :-) Be warned !

Extension exercise - put a Rich Text Box on each Form and adapt the above code to allow RTF to be copied to the clipboard, and then displayed in the second app.

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

Hi again "Pearlers",

This time we are going to see how an image may be PUSHed to the clipboard from one app, and PULLed down by another, and quite separate app.

The attached image shows just this case.

The jpeg image of the five icons which is applied to the picture box in the InstallVOUG app, is eventually copied from the clipboard and applied to the smaller picture box in the "Has - Dictionary" demo app.

The pull-down and application is triggered by a push button click event on the "Get Clipboard Image" button.

//-------------------- put icons JPG on the form ----------------------

LOCAL oImage AS Image

oImage := System.Drawing.Image.FromFile("c:\vdnuk\fiveicons.jpg")

SELF:opbFiveIcons:Image := oImage

Clipboard.Clear()

Clipboard.SetImage(oImage)

SELF:opbFiveIcons:SizeMode := PictureBoxSizeMode.StretchImage

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

The above code applies the image and pushes a copy of it to the clipboard.

Finally, the code below is what is required to attach to the push button in the second app.

METHOD pbGetClipboardImageClick(o AS OBJECT , e AS EventArgs) AS VOID

LOCAL oImage AS Image

IF Clipboard.ContainsImage()

oImage := Clipboard.GetImage()

SELF:opbClipboard:Image := oImage

SELF:opbClipboard:SizeMode := PictureBoxSizeMode.StretchImage

ELSE

MessageBox.Show(SELF,"No image can be found in the clipboard

!","No Data Present :")

ENDIF

RETURN

If you change the demo apps so that one allows selection of an image from a set, then the second image is just "polling" the clipboard for suitable images, it could display any that it finds ASAP. This would make a suitably impressive demo for Vulcan.

Good luck with your imaging :-)

Have a look at the section on "Running Vulcan from VO" as this has a nifty idea for using the clipboard to return information to the launching VO app.

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

Hi Pearl Pickers,

I have just posted a few more tips to Wiki - but since the formatter may need to be reinstated, have come back to "Pearls" for a more visual experience.

The image attached shows two apps running, side by side, and "no strings attached" - as they say in the UK :-)

If you look carefully you will see that bottom left of the first - shows the text "Hi Chris this works !!!" was typed into a text box.

The push button then sent this simple text to the clipboard.

In the second, and separate app, the push button "Pull from Clipboard" when clicked, looked to see if suitable text was resident in Windows clipboard, and then when some was found, copied it down and displayed it in the Text Box above the Pull button.

The code to do this is shown alongside. It is quite easy to follow.

Try doing your own demo app where one actually "polls" the clipboard for suitable text - no need for button pushing - just a timing loop.

This will look very good on two monitors - for a demo display.

Anything you type in one app (screen) automatically appears on the other :-)

Regards,
Phil.

P.S. another "Pearler" for images in the clipboard.

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


Section 8 - Drag and Drop


First Attempts and Success

Hi friends and colleagues,

I will share with you some very useful code a nice man from Greece sent me to get me going with my first Drag and Drop ever !

Not done this before in VO.

I studied information in books on WinForms and they seemed to make it sound all very difficult and complicated. The GREAT news is that is isn't if you go about it in the right way.

Below is the code which generated the images attached, so will be useful to get you going with DnD as it did for me.

I will digest things a little more and then try to put as simple an approach as possible to this on Wiki.

Most of us wanting to try DnD will appreciate roughly what the events are doing for us "behind" the form and controls. But, once a developer realizes that the "crunch" to this is having a class (albeit simple) which will act for the Form a bit like the "clipboard", and hold information on the source of the Drag and the item selected, it all becomes a lot more understandable - in my eyes!

{ >> think of it as a global virtual work space - as the "clipboard" is a specific thing with defined functionality << }

You then can see places in the code where you can make changes to turn a MOVE into a COPY, or to disallow Drag or Drop from certain ListBox controls.

Finally, you get carried away and want to do DnD on everything else - BUT - let's take it a step at a time for the standard guys like me.

PP has offered to help if and when I hit the wall - thanks Paul :-)

Hope this is as helpful (and fun) to you as it was to me.

Phil. (I will attach the PRG text for your use.)

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

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

PHH with CP : Oct. 2007


PRG - working code - First

USING System.Windows.Forms

USING System.Drawing

[STAThread];

FUNCTION Start() AS VOID

Application.Run(DragDropForm{})

RETURN

// a custom class describing drag & drop operations

CLASS DragDropInfo

EXPORT oSourceControl AS ListBox

EXPORT nItemIndex AS INT

CONSTRUCTOR(oControl AS ListBox , nIndex AS INT)

SELF:oSourceControl := oControl

SELF:nItemIndex := nIndex

RETURN

END CLASS

CLASS MyListBox INHERIT ListBox

CONSTRUCTOR()

SUPER()

SELF:AllowDrop := TRUE

RETURN

PROTECTED METHOD OnMouseDown(e AS MouseEventArgs) AS VOID

LOCAL oInfo AS DragDropInfo

SUPER:OnMouseDown(e)

IF e:Button == MouseButtons.LEFT

IF SELF:SelectedIndex != -1

oInfo := DragDropInfo{SELF , SELF:SelectedIndex}

// this starts the drag & drop operation

SELF:DoDragDrop(oInfo , DragDropEffects.Move)

ENDIF

ENDIF

RETURN

PROTECTED METHOD OnDragEnter(e AS DragEventArgs) AS VOID

LOCAL oInfo AS DragDropInfo

SUPER:OnDragEnter(e)

IF e:Data:GetDataPresent(typeof(DragDropInfo))

oInfo := (DragDropInfo)e:Data:GetData(typeof(DragDropInfo))

IF oInfo:oSourceControl != SELF

// allow move only

e:Effect := DragDropEffects.Move

RETURN

ENDIF

ENDIF

e:Effect := DragDropEffects.None

RETURN

PROTECTED METHOD OnDragDrop(e AS DragEventArgs) AS VOID

LOCAL oInfo AS DragDropInfo

LOCAL oItem AS OBJECT

SUPER:OnDragDrop(e)

IF e:Data:GetDataPresent(typeof(DragDropInfo))

oInfo := (DragDropInfo)e:Data:GetData(typeof(DragDropInfo))

IF oInfo:oSourceControl != SELF

oItem := oInfo:oSourceControl:Items[oInfo:nItemIndex]

oInfo:oSourceControl:Items:Remove(oItem)

SELF:Items:Add(oItem)

ENDIF

ENDIF

RETURN

END CLASS

CLASS DragDropForm INHERIT Form

PROTECT oListBox2 AS MyListBox

PROTECT oListBox1 AS MyListBox

// User code starts here (DO NOT remove this line) ##USER##

CONSTRUCTOR()

SUPER()

SELF:InitializeForm()

SELF:oListBox1:Items:Add("a1")

SELF:oListBox1:Items:Add("a2")

SELF:oListBox1:Items:Add("a3")

SELF:oListBox2:Items:Add("b1")

SELF:oListBox2:Items:Add("b2")

SELF:oListBox2:Items:Add("b3")

RETURN

METHOD InitializeForm() AS VOID

//--- IDE generated code (please DO NOT modify) ---

SELF:Name := "DragDropForm"

SELF:SuspendLayout()

SELF:Location := Point{100,100}

SELF:ClientSize := Size{490,277}

SELF:TEXT := "Form"

SELF:oListBox2 := MyListBox{}

SELF:oListBox2:Name := "ListBox2"

SELF:oListBox2:Location := Point{ 253 , 27 }

SELF:oListBox2:Size := Size{ 185 , 199 }

SELF:oListBox2:TabIndex := 1

SELF:Controls:Add(SELF:oListBox2)

SELF:oListBox1 := MyListBox{}

SELF:oListBox1:Name := "ListBox1"

SELF:oListBox1:Location := Point{ 26 , 27 }

SELF:oListBox1:Size := Size{ 185 , 199 }

SELF:oListBox1:TabIndex := 0

SELF:Controls:Add(SELF:oListBox1)

SELF:ResumeLayout()

RETURN

END CLASS


Improved DnD Functionality

Enter topic text here.


PRG - working code - Improved

Enter topic text here.


Section 9 - Namespace and References

One of the hardest things to get used to at first when coming to .NET is the fact that you will not get your code to compile if you do not provide the correct namespace and references.

I am speaking with only major experience of VIDE for supplying the References as this is done through the IDE itself.

A new user is keen to see their programming efforts compile and work, and yet these red messages can keep popping up to frustrate you.

USING System.Globalization

CultureInfo class // find out about ...

Include some basic pointers on what namespaces are likely to be required, as well as the References in the VIDE - show pictures.

Tell how to find out in SDK doc and how to make the changes.

Also mention attributes and give an example.

Serializeable; IComparer ??

System.DirectoryServices.Protocols

USING System.Windows.Forms

USING System.Drawing.Printing

//USING VulcanDB

//USING VulcanDBFCDX

USING System

USING System.IO

USING System.Text

USING System.Collections

USING System.Web.UI.WebControls

USING System.Drawing

USING System.Drawing.Drawing2D

//using Microsoft.Office.Tools

#DEFINE cR CRLF

#DEFINE cR2 cR+cR

USING System.Windows.Forms

USING System.Drawing

USING System.Diagnostics

USING Microsoft.Win32

USING System.Collections

USING System.IO

USING System.Text

USING System.Threading

USING System

USING System.Data

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

USING System.Drawing

USING System.Collections

USING System.IO

USING System

USING System.Windows.Forms

USING Microsoft.VisualBasic.Devices

//USING System.Collections

USING System.Drawing.Printing

//USING System.Collections.Generic

//USING System.Data

[Serializable];

CLASS DragDropInfo

EXPORT oSourceControl AS ListBox

EXPORT nItemIndex AS INT

EXPORT lAllowDelete AS LOGIC

CONSTRUCTOR (oControl AS ListBox, nIndex AS INT, lDeleteOK AS LOGIC)

SELF:oSourceControl := oControl

SELF:nItemIndex := nIndex

SELF:lAllowDelete := lDeleteOK

RETURN

END CLASS


Section 10 - Printing : Basic Output to Page

Getting Basic Elements on the Page :

It is not a difficult job to get some basic elements printed on a page and have this display in the Print Preview dialog box, as well as printed to paper.

However, it takes a lot more thought and effort to "control output" to a page in any meaningful way. The majority of the 'BasicDocument' class I have written (see Section 3) is concerned with the easy control of output to page.

In this small section we will see how easy it is to get information printed out.

My first attempts and successes were built on some DevFest notes from Chris Pyrgas - I will use these again as a basis here.

Below is the test page from my Examine Printers app - which simply sends a test page to the printer being tried and tested. The information on the page is just the settings of the printer, both default and those chosen by the user from the GUI screen. This test page is done in the simplest way, following Chris's notes.

We will now look at the crucial parts of the code to see just how a page may be printed, and what the basic elements are that this approach requires. Notice the print out is shown in a dotNet Print Preview dialog screen.

On the GUI form there is a push button called "Do Test Print" and the click event has the following handler :-

METHOD pbDoTestPrintClick(o AS OBJECT , e AS EventArgs) AS VOID

//------- force the print of a test document - with full details on ! -------

LOCAL oDocument AS PrintDocument

LOCAL oPreview AS PrintPreviewDialog

oDocument := PrintDocument{}

oDocument:PrintPage += PrintPageEventHandler{SELF, @TestPagePrint()}

IF ocbApplyUserSettings:Checked

SELF:ApplyChosenSettings(oDocument)

ENDIF

IF ocbPreviewOnly:Checked

oPreview := PrintPreviewDialog{}

oPreview:Document := oDocument

oPreview:PrintPreviewControl:AutoZoom := TRUE

oPreview:ShowDialog()

ELSE

oDocument:Print()

ENDIF

oDocument:PrintPage -= PrintPageEventHandler{SELF, @TestPagePrint()}

oDocument := NULL_OBJECT

RETURN

This first method is the one which is triggered by the mouse click of the push button. You will notice that depending on the checked status of the "Preview Only" check box, the result of the mouse click is either hard copy print out or a print preview screen.

The object required for printing is an instantiation of the PrintDocument class. Once this object is 'set up' correctly it is either passed into the "Preview" instantiation of the PrintPreviewDialog class as a property, or it's 'Print' method triggers the PrintPage event.

Notice that the PrintPage event handler needs to be set and here we call the TestPagePrint method in response to the event tigger. If you like you can think of the ShowDialog() method for the Preview diaolog, as a virtual print. So both the ShowDialog() and Print() methods trigger the handler to come into operation.

Below is this event handler method.

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

LOCAL oFont AS Font

LOCAL oBrush AS SolidBrush

LOCAL x,y,nField, i AS DWORD

LOCAL cLine, cDate, cTime AS STRING

LOCAL oDT AS DateTime

oDT := DateTime.Now

oFont := Font{"Arial" , 10.0}

oBrush := SolidBrush{Color.Black}

y := 50 //--------- choose a suitable position ---------

x := 50 //--------- top left of the paper ---------

cLine := "Examine Printers Test:-"

e:Graphics:DrawString(cLine , oFont , oBrush , x , y)

y += oFont:Height

y += oFont:Height

cDate := oDT:ToLongDateString()

cTime := oDT:ToLongTimeString()

cLine := "Date : "+cDate+" Time : "+cTime

e:Graphics:DrawString(cLine , oFont , oBrush , x , y)

y += oFont:Height

cLine := "================================================"

e:Graphics:DrawString(cLine , oFont , oBrush , x , y)

y += oFont:Height

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

oBrush := SolidBrush{Color.Blue}

cLine := "Chosen Settings Below :-"

e:Graphics:DrawString(cLine , oFont , oBrush , x , y)

y += oFont:Height

y += oFont:Height

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

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

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

FOR i := 1 UPTO 12

cLine := SPACE(5)+aDS[i]:ToString()

e:Graphics:DrawString(cLine , oFont , oBrush , x , y)

y += oFont:Height

NEXT // i

e:HasMorePages := FALSE

RETURN

This may look a bit daunting but, it is really a sequence of simple steps.

PHH December 2007.


Section 11 - Printing - the Fundamentals

Printing in a Windows Environment with Modern Printers :

Gone are the old days when you sent a bunch of characters to the printer and something came out on paper.

In those early days of printing any control of the printed output was done at, and usually by, the printer. All the software could do was insert a few characters (or group of characters) into the text to be output, telling the printer what to do. For example a pair of characters "carriage return and line feed".

These 'control' characters were termed non-printable characters, and were outside the binary range of those that were used for text and graphics.

In may ways the RichTextBox acts like these early printers. The pair of characters with code 12 and 13 represented carriage return line feed or CRLF as it may be used in Vulcan.

Here is a list for useful reference :- (called ASCII code)

You will notice that a tab is character 11 so if we send a CHR(11) to our RichText control on a GUI we get a tab shift.

Spaces on the other hand are printable characters, and often a tab is taken as five spaces. A space is CHR(32) and comes at the head of the next column of characters after that displayed above.

In my code shown for many parts of this ClickStart you will see the MessageBox sent CRLF in the same way as discussed above. The MessageBox also responds to control characters in a similar way to the RichText control.

However, in the modern world of printers and printing, the page can be thought of as an artists canvas. Anything can be placed anywhere, as long as it is on the canvas !

Also, one object may be place over another which has already been placed on the canvas. Characters and text are just objects to be placed.

When an early printer received the code 65 or the hexadecimal code 41, it printed an upper case 'a' at the place where the print head happened to be, and then the head moved on, controlled by the printer mechanisms.

In today's world of printing, an upper case 'a' can almost be one of an unlimited number of possible shapes and sizes. And, don't forget colour too !

Now that printers no longer have physical and functional parts called character generators, they simply produce the artist's canvas image as sent by the operating system.

So CHR(65) would not spark the printer into life as it once would. These printers when they first came out were called Windows printers - when they may have been better described as printers without a character generator !

If you have a matrix printer printing pre-printed stationery in your office, it will still have a character generator inside. The data sent to it will need control characters inserted into the data stream.

Now you may see why printing on a modern Windows computer needs a lot more thought. We, and the software we write, have to control the whole of the layout on the page canvas, before we say PRINT please.

PHH December 2007.


Section 12 - Third Party External Apps

Launching Word, Excel, Reader - Third Party External Applications :

One of the attached images shows MS Word open with a document for the VOUG members to see.

The "slide show" applet effectively does a "ShellExecute", as the secondary file name determines which main application is launched.

So an .RTF file launches Word. This is not particularly different in any way to VO etc. However, we can now easily control the closing of the external application.

First we will look at the method to cycle through the slide "file names" held in the array SELF:Slides[] > note that the Slide Show screen is designed such that one button "opens" the slide and another "closes" it.

Both NEXT and LAST "slide" buttons do an "open" - obviously the CLOSE slide button does the "close".

The "close" method is below the next method and slide filling code.

METHOD ShowSlide() AS VOID

LOCAL cSlide AS STRING

IF SELF:SlidePointer > SELF:NoOfSlides //---------- cycles through the slide set ------------

SELF:SlidePointer := 1

RETURN

ENDIF

SELF:oftPointer:TEXT := "Slide Pointer :"+NTrim(SELF:SlidePointer)

cSlide := SELF:Slides[SELF:SlidePointer]

SELF:SlidePointer +=1

//MessageBox.Show("First slide is :- "+cSlide)

IF File(cSlide)

oProcess := Process{}

oProcess:StartInfo:FileName := cSlide

oProcess:StartInfo:WorkingDirectory := ""

oProcess:StartInfo:UseShellExecute := TRUE

oProcess:Start()

SELF:SlideShowing := TRUE

SELF:opbNextSlide:Enabled := FALSE

SELF:opbLastSlide:Enabled := FALSE

ELSE

SELF:SlideShowing := FALSE

SELF:opbNextSlide:Enabled := TRUE

SELF:opbLastSlide:Enabled := TRUE

ENDIF

RETURN

Below the code for filling the Slide Array is shown and that for putting the slide names into the Combo Box, in a more familiar fashion for VO developers, no FillUsing() however!

SELF:Slides := {}

//----------------- intro ----------------

AADD(SELF:Slides,"c:\voug_cd\textfilesncoding\TheShapeOfTheEvening.rtf")

//-------------- hand held --------------

AADD(SELF:Slides,"c:\voug_cd\filesnpdf\DSCF0070.pdf")

AADD(SELF:Slides,"c:\voug_cd\filesnpdf\CPtoolTipsaDream.pdf")

//-------------- use of Excel ---------------

AADD(SELF:Slides,"c:\voug_cd\filesnpdf\BESTtest.csv")

//--------------- handy tips ---------------

AADD(SELF:Slides,"c:\voug_cd\textfilesncoding\HandyTips.rtf")

.

.

FOR i := 1 UPTO ALen(SELF:Slides)

cSlide := SELF:Slides[i]

SELF:ocbSlides:Items:Add(cSlide)

NEXT // i

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

METHOD CloseSlide() AS VOID

SELF:opbNextSlide:Enabled := TRUE

SELF:opbLastSlide:Enabled := TRUE

IF SELF:SlideShowing

IF ! oProcess:HasExited

oProcess:CloseMainWindow()

oProcess:Close()

ENDIF

oProcess := NULL_OBJECT

SELF:SlideShowing:= FALSE

ENDIF

RETURN

You will see that the combo box holding the slide names can also be used to launch the selected slide, but the first thing this does after selecting the name of the slide and setting the pointer, is to make sure the "process"started previously, is finished.

METHOD cbSlidesSelectionChangeCommitted(o AS OBJECT , e AS System.EventArgs) AS VOID

LOCAL cSlide AS STRING

LOCAL nIndex AS LONG

cSlide := SELF:ocbSlides:SelectedItem:ToString()

nIndex := SELF:ocbSlides:SelectedIndex + 1

SELF:SlidePointer := nIndex

SELF:CloseSlide()

Application.DoEvents()

System.Threading.Thread.Sleep(1300)

Application.DoEvents()

SELF:ShowSlide()

RETURN

I hope I have not left any important parts out :-)

Please note that when the file is opened in the external app, the mouse scroll wheel actually moves the display in this external app without any prior selection.

That means the focus must be moved to the "process" screen. Very handy for slide shows ! (I tested this on Vista 32 business.)

This is best seen on a two monitor PC set-up where the "Slide Show" runs on one monitor and the slide output is displayed on the other.

At first, I did not handle the event of the user (me) closing the external window by mouse clicking the red "Query Close" box.

This gave an error the next time the "Next" button was clicked.

This is now handled OK.

Note that Excel is opened for CSV files - see fourth image. It is nice to be able to close Excel from within the calling application :-)

{*** SPECIAL note - remember that VO apps are also launched in this way too !}

During my tests I came across an issue with closing the "Vista : Windows Photo Gallery" while displaying JEPGS, I got round this by not using it and making PDFs, I would be very interested in an answer or solution, or both.

Since I just fudged the issue to get something working.

PHH: Oct.2007

=================================================================================
PPwordFromVDN

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

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

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

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


Section 13 - External Apps & Exit Codes

Launching and Controlling External Applications from VDN :

The ability in Vulcan.NET, to be able to launch any external application has never been simpler. It is at least as easy as VO but you also get more more control and a way to easily get information back from the external application as it closes.

The .NET class required is the "process" class.

It allows the developer to not only launch a third party app., but also to have more control than in VO - as far as I am aware.

Once an app has been launched it can also be "cleanly" closed, and also any exit code from the third party app received and processed by the VDN code still running.

We may have tried to use "ShellExecute()" from VO to do some of this sort of "stuff" in the past. Okay, but this is better.

For our example to study, let us consider using "WinRAR" to make a backup of a set of DBF files starting with the letter "C".

The attached image (to come) shows the "Slide Show" demo of mine, with a button to start the WinRAR backup using code similar to that below.

LOCAL oProcess AS Process

LOCAL cFile, cWhere, cWhat, cText AS STRING

LOCAL nCode AS LONG

LOCAL cStartTime, cFinishTime, cElapsedTime AS STRING

LOCAL dtFinish, dtStart AS DateTime

LOCAL tsCheck AS TimeSpan

cStartTime := LEFT(Time24(),8) //-------------- there are better .NET ways

//to deal with time and differences ! -------------

cFile := "C:\1_aPelican2006app\WinRAR\WinRAR.exe"

cWhere := "C:\1_aPelican2006sys\backup\"

cWhat := "C:\1_aPelican2006dat\pelican2006\c*.dbf"

oProcess := Process{}

oProcess:StartInfo:FileName := cFile

oProcess:StartInfo:WorkingDirectory := ""

oProcess:StartInfo:Arguments := "a -m3 "+cWhere+SPACE(1) +cWhat //------------- these command line parameters

// are governed by WinRAR specification -------------

oProcess:StartInfo:UseShellExecute := TRUE //---------- so can't use Responding property ! ----------

oProcess:Start()

DO WHILE ! oProcess:HasExited

System.Threading.Thread.Sleep(1300)

ENDDO

nCode := oProcess:ExitCode

dtFinish := oProcess:ExitTime

cFinishTime := dtFinish:TimeOfDay:ToString()

cFinishTime := LEFT(cFinishTime, 8)

cElapsedTime := ElapTime(cStartTime, cFinishTime)

IF nCode = 0

cText := "Backup Completed Successfully !!!"

ELSE

cText := "User Intervention, exit code is :- "+NTrim(nCode)+CRLF

cText += "Time when started :- "+cStartTime+CRLF

cText += "Time when finished :- "+cFinishTime+CRLF

cText += "Elapsed time is :- "+cElapsedTime

ENDIF

MessageBox.Show(cText)

Notice that in this example the VDN code is waiting for the external app to finish. (Sleep does not work quite how it used to, see Section 29 sub-section on Pausing an Application.)

The way this happens is either for the backup process to complete, or the execution to be "stopped" in some way.

The value of the exist code tells what did happen, and a list of these values are shown in the image available(to come), there is a button to display a message box.

This is probably enough for one Wiki article - I will cover launching MS Word and Excel in a separate additional Wiki/Pearl article. As well as launching VO apps too :-)

PHH: Oct.2007

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

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

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

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


Section 14 - Running Vulcan from VO

Launching a Vulcan application from a running Visual Objects application : ( In a modal fashion. )

One of the easiest ways to "upgrade" an existing VO application to Vulcan.NET is to add the facility to launch a completely new VDN application from within the VO code.

I have successfully placed a new button on a printing screen in my VO application, and linked this to launching a VDN program which also uses a command line parameter telling the VDN where a config. file has been placed by VO for its use after launch. Paul Piko's book gave me the clues.

I then have my VO app wait while the VDN form is still open, making the Vulcan app appear to be modal in its operation. The VO code shown keeps looking to see if a window exists with a caption entitled "complex ..." as this is in the Form 'caption bar' of the VDN application being used.

Below is the VO code to launch the VDN app with a command line string which is the folder name of the config. file later required to open the file, and therefore access the data and data files.

The EXE file being launched is called "ScreenPrinting" and till we get images attached to this you will have to believe me that the main Form of this app has a caption of "Complex Report Printing :".

You will notice that the VO code goes into a loop until the function "MyDotNETOpen" is FALSE. You can modify the code shown here to allow the caption to be passed as a parameter to the function. (A left portion of the caption will do.)

We will look at the code in this "checking" function after showing that discussed so far.

cMyFolder := "Print2FileDotNET\"

cFolder := guWkTemp+cMyFolder

cFile := "Vulcan_Config_Simple.PHH"

cRunLineVulcan := [c:\VIDE\Projects\BasicVulcanPrinting\BEST_VO\ScreenPrinting.EXE ]+cFolder+SPACE(1)+cFile

oApp := App{}

oApp:Run(cRunLineVulcan)

oApp := NULL_OBJECT

//------------- add the external testing loop -------------

PointerHG(SELF,) //------- you could hide the pointer too ! -------

Sleep(1000)

DO WHILE MyDotNETOpen()

Sleep(1000)

ENDDO

PointerA(SELF,)

So far so good, we have a VO calling section followed by a checking section, which waits until the external VDN app is closed, before continuing.

What now is this function "MyDotNETOpen" all about ? Well I have to confess it was code I was given by a helpful man in Bristol, who said it had come to him from Australia. Or the idea had - not sure which really :-) All I know is I have used it effectively for quite some time, in different places in my VO apps. It works great.

Put simply, it looks to see if there is a shell (main) window open in those known to the Windows operating system, with the caption starting with "COMPLEX" when forced to an upper case comparison.

Because it was a bit of demo test code, I did not get round to passing into the function the caption title being searched for - I will leave that bit up to you, as you are getting enough code anyhow ! That's my excuse.

FUNCTION MyDotNETOpen() AS LOGIC

LOCAL lReading AS LOGIC

LOCAL hWnd AS PTR

LOCAL sValue, cText, cFound, cMess, cLeft AS STRING

LOCAL ptrBuffer AS PTR

LOCAL wLen AS DWORD

LOCAL i AS INT

LOCAL aWins := { } AS ARRAY

lReading := FALSE

LEN := 100

ptrBuffer := MemAlloc(wLen)

hWnd := GetTopWindow( NULL_PTR )

DO WHILE ! hWnd == ( NULL_PTR )

GetWindowText( hWnd, PSZ(_CAST,ptrBuffer), wLen)

sValue := Psz2String( PSZ(ptrBuffer) )

AADD( aWins, { hWnd, sValue } )

hWnd := GetNextWindow( hWnd, GW_HWNDNEXT )

ENDDO

MemFree(ptrBuffer)

cMess := ""

FOR i := 1 UPTO ALen( aWins )

cText := UPPER(aWins[i,2])

IF cText == ""

LOOP

ENDIF

cLeft := LEFT(cText,8)

IF Instr(UPPER("Complex"), cText)

lReading := TRUE

ENDIF

NEXT // i

RETURN lReading

It may be of interest to you to know that is is very easy to modify this approach to be able to know from within a VO app, just which external applications are running. I have used it for Word, Excel, Adobe Reader, Notepad, Wordpad and many more, including my own apps. It is one way to stop multiple launches of the same VO app if you wish to do so.

A final word of warining, the file manager 'Windows Explorer' does not have a fixed caption bar as it displays nothing but the file/folder path, which changes as it is used!

Look out for an article on detecting external apps running from VO. I will include the code to do as described above.

By using the approach shown in this article, you may write a new "module", or chunk of program, which is fully Vulcan and .NET in its style and approach, and use it from existing VO27b/VO28 apps. without any modification or recompiling other than adding the required code lines above to the existing VO code.

I think this adds a great ability, and flexibility, to the pallet of ways of going forward into a new world, without having to go backwards a great long way first. (I am alluding to recoding, or Transporting, all existing apps!)

Out of interest I have a demo app called "Slide Show" which is VDN, and at one point it launches a large VO app which, from one of its printing screens, has an option to print via a VDN applet. So we have VDN - to - VO - to - VDN, and the user can't even see the joins.

I hope this helps you find some interesting solutions.

PHH: Oct.2007


Using the Clipboard

<< Remember to include the use of the clipboard to return a string from a custom made VDN application. >>


Section 15 - Data Files - DBFCDX

Many people coming from the world of Visual Objects will have used, and will probably still be using, DBF data files and their related indexes CDX.

I will discuss only the DBFCDX format in this short section - really just covering what I needed to do to successfully adapt my programming style to the Vulcan.NET environment.

Needless to say, all things seem to be working fine with both data files and indexes. We had a few rocky rides in the beta testing but here we are :-)

First of all, an important point which seems to have caught out a few others judging by the news group messages, include the following line :-

RDDSETDEFAULT("DBFCDX")

in your code. I am note sure on all the specific details. but, I have found that if I put it in each class I have which uses DBFCXD technology, then all works OK.

It does not hurt to have it more often than may be really necessary - just don't be short, or you will get an error box.

CONSTRUCTOR()

SUPER()

SELF:InitializeForm()

RDDSETDEFAULT("DBFCDX")

Placing it in the Constructor, right after the form initialisation seems to do the trick. No doubt we will get smarter and more knowledgeable in time.

Secondly, there seemed to be many styles in VO which worked successfully for developers, some swore by one approach, and others by another. I followed some advice from WM at a DevFest presentation and it has served me very well for years - thanks Willie.

However, when I came to Vulcan I found I needed to adapt my coding style and change a few things. So I now stick with what seemed to work in earlier beta releases of Vulcan, and what still works now.

First of all, lets open a file :-

cPathFile := "c:\VDNUK\PatAnon32.dbf"

IF ! File.Exists(cPathFile)

MessageBox.Show(SELF, "Data file could not be opened :- "+cPathFile, "WARNING !!!")

RETURN

ELSE

MessageBox.Show(SELF,"Great - data has been found.","SUCCESS !")

ENDIF

oServerPA := DBServer{cPathFile}

oServerPA:SetIndex("c:\VDNUK\patanon32.cdx")

oServerPA:SetOrder("patunref") //--------- alternate SetOrder(1) --------

oServerPA:GoTop()

I am not sure whether it is now necessary to set the index and the order, but since I got into the habit to solve an earlier problem I like to think I am taking control rather than trusting the system to do things for me.

And now close it :-

IF ! oServerPA = NULL_OBJECT

oServerPA:Close()

oServerPA := NULL_OBJECT

ENDIF

Nothing special I hear some of you say ! That is true, however when we need to access and assign data to and from the fields, this is where I have changed my approach.

DO WHILE ! oServerPA:EoF

cSurname := oServerPA:fieldget("surname")

nPatRef := oServerPA:fieldget("patunref")

lActive := oServerPA:fieldget("patactive")

nLastOrderNum := oServerPA:fieldget("lastorder")

cPostCode := oServerPA:fieldget("pcode")

dDOB := oServerPA:fieldget("dob")

dLastOrder := oServerPA:fieldget("lastordate")

I now always use fieldget and fieldput, whereas I most often used to use the syntax such as :-

cPostCode := oServerPA:pcode

Since I feel that using the field methods is more in keeping with the modern object style I am happy to do this. Quite surprised I am saying this really :-)

Even though you may be experienced at using DBFs I suggest you have a trial in all the various aspects of their use before you need to use them in anger !

I have used create index methods, and done seeking on all types of data allowed in DBFs, and all now seems familiar.

I wish I could give more help and advice but the fun of .NET took me to new pastures ! I am sure the trusty old DBF will not let you down, but try to keep to using the common DBFCDX format as this seem to be the most likely to not let you down.

One interesting aspect is how we may NOT mix dotNet methods directly on the DBF field contents. This is because they are USUAl until they are cast into other receiving variables.

In the rest of my code I am trying to use dotNet style methods in place of VO functions - but here we have to be careful.

Let us take an example, look at the post code field I have just read in. I would like to have trimmed off leading and trailing spaces at time of reading - as I used to in VO.

cPostCode := oServerPA:fieldget("pcode")

The line above shows what I am reading in, however if I code it as below :-

cPostCode := oServerPA:fieldget("pcode"):TRIM()

then I will get an error at compilation time as shown :-

So we will have to either still use VO functions on our USUAL values as previously, or do our processing and tidying after assignment.

cPostCode := AllTrim(oServerPA:fieldget("pcode"))

cPostCode := oServerPA:fieldget("pcode")

cPostCode := cPostCode:TRIM()

Here is a snap shot after changing the code and recompiling. Notice we get no errors.

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

So it is up to us whether we give up VO functions altogether. After some months now at learning Vulcan, I feel it is easier to move to one approach on as many things as possible.

I feel that as long as I need to use DBFs, I will read in their field values first, and after assignment make my dotNet style adjustments. I am to give up VO function style coding and will make all my own useful functions into static methods as shown in Section 26.

Note that some of the lines above are superfluous but are there for demonstration purposes only.

PHH December 2007.


DataTable dump to DBF

Since the DataTable class is part of dotNet it has many methods associated with filling it with data from suitable and varied sources.

There are also ways to set a schema for the table - which specifies column data types etc.

However, since DBF technology is from a different era then it will be a little lacking generally in the .NET framework :-)

So we need to develop ways to help yourselves when we still wish to use DBFs.

It would appear to me, after doing some initial work and research on the matter, that being able to "dump" data from an existing DataTable into a newly created DBF would be a very useful tool to have available to us.

For now will will just deal with string types for each field. Once we get used to using data tables, data rows, and data row collections, then we may go further to see what else we can learn about Data Tables and their use.

By "dumping" table data, the developer can make DBFs from virtually any data going. I realise we may have to be aware of more advanced data types than those we have available to us in the DBF format. Such as DateTime, TimeSpan and others, but we can worry about such matters when we get that far.

For now, let us just see how we can code so that we can copy table data into a DBF, where we make the structure ourselves, and then fill the records directly from the table rows, and hence cells.

For this example I will use the DataGridView demo application shown in section 25.

First of all let us look at how to use all the column headers in the table to create a DBF structure for our file. Later this will be filled with data.

What we wish to know for a DBF structure is an array of information which specifies the name of the field, its length, the type of data item stored, and the number of decimal places used.

Since we are going to start by "dumping" data as just strings, the the type is "C" for character, and zero for the number of decimal places. So that only leaves the field name and field width. Obviously the hard one to find is the width of the field. The name is already "attached" to the column item.

When we say field width, the best we can do is look at all the data items in a specified column. The widest data item will give us a value for the width of our field. If we then make our DBF with a structure based on the maximum widths found in this way, we will at least be able to fit all the table data into the fields so created in our DBF.

Below is shown some code which looks at the data in the table and examines its width to make an array which then has all the maximum values required. Take a look at what this looks like below :-

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

Now we can use the array of max values to create our DBF structure array, along with the field names. Let us see the sample of code to find the field names first of all :- ( we will cal the Table Column Names to distinguish from an earlier output.)

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

Users new to dotNet and familiar with Visual Object coding, may need to take a short while to get used to these new classes, and ways of doing things. So this sort of exercise is good to do before we start to want to specify data types for the table columns, and much more. Once you get into using Data Tables you will wish to do so much more - BUT - as we said before, one step at a time !

Okay, now we have all the data we require to make a structure for our "to be" DBF, and then to create an empty file with this structure.

Lets go for it !

METHOD CreateDBFstructure() AS VOID

LOCAL aDBF AS ARRAY

LOCAL nWide, i AS DWORD

LOCAL oDC AS DataColumn

LOCAL cName AS STRING

aDBF := {}

FOR i := 1 UPTO SELF:NumberOfFields

nWide := SELF:aFieldWidths[i]

oDC := SELF:oDCC:Item[i-1]

cName := oDC:ColumnName

AADD(aDBF, {cName, "C", nWide, 0})

NEXT

DBCreate("C:\VDNUK\dump2dbf",aDBF,"DBFCDX")

RETURN

The code shown above seems to do the trick nicely. Before we look at how to call the class into action we can first spend a few moments viewing the results of our developed code.

Below is shown the output from running our code :-

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

Note the fields created - using dBAdministrator to do the job. If you have got dBAdministrator I suggest you buy one from Eddi Celebi as it is a great tool and one which I use as much as anything in my work. It is easy to buy a copy over the internet.

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

For those of you who like a little safety in what you do, I have just thought, it may be a good idea to add one, or two, extra to the field widths, since the values found are only just as wide as the largest data item.

Below is the code to instantiate and use the CSVtoDataTable class. Since the DBF creation only operates on an existing table we can do and test the code development without need to use the WarpGrid demo app to display anything. The WarpForm lines are just there for me to use with other related things in developing these notes this morning.

FUNCTION Start( asCmdLine AS STRING[] ) AS INT

LOCAL nExitCode AS INT

LOCAL oForm AS WarpForm

//LOCAL oCSV AS CSVfromDBF

LOCAL oC2DT AS CSVtoDataTable

//oCSV := CSVfromDBF{"MyCSVtest", "C:\VDNUK\", "patanon32", "C:\VDNUK\"}

//oCSV:DoConvert()

//oCSV := NULL_OBJECT

oC2DT := CSVtoDataTable{"MyCSVtest", "C:\VDNUK\", "MyCSVTable"}

oC2DT:FindFieldWidths()

oC2DT:FindColumnNames()

oC2DT:CreateDBFstructure()

oC2DT := NULL_OBJECT

Application.EnableVisualStyles()

Application.DoEvents()

oForm := WarpForm{}

Application.Run(oForm)

RETURN nExitCode

Right the, now for the code which will dump the table data into the newly made DBF.

All we need to do is open the DBF for writing into, then append a record for each row of the table. Then after getting each row, move along the row finding the cell contents and write them to the correct field in the DBF.

Lets see how this will look.

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

Please note that in order to get my external processes to synchronise with the code being executed, I had to revert to a few old VO tricks to get things to work !

Eventually the operating system did catch up and the DBF was opened for writing the data.

And now the code which finds and copies the data from the rows and cells :-

METHOD OpenAndDumpData() AS VOID

RDDSETDEFAULT("DBFCDX")

LOCAL oODD AS DBServer

LOCAL cFile, cFilePath, cDBF, cContent AS STRING

LOCAL i, k AS DWORD

cDBF := "dump2dbf"

cFilePath := "c:\VDNUK\"+cDBF

IF File.Exists(cFilePath)

MessageBox.Show("File found ...")

ELSE

MessageBox.Show("File NOT found !!!")

ENDIF

oODD := DBServer{cFilePath}

IF oODD:Used

MessageBox.Show("Open for use ...")

ELSE

MessageBox.Show("NOT Open for use !!!")

ENDIF

oODD:GoTop()

FOR i := 0 UPTO SELF:MaxRows-1

oDR := oDRC:Item[i]

oODD:Append()

FOR k := 0 UPTO SELF:NumberOfFields-1

cContent := oDR:Item[k]:ToString():TRIM()

oODD:FIELDPUT(k+1, cContent)

NEXT

NEXT

oODD:Commit()

oODD:Close()

oODD := NULL_OBJECT

RETURN

This seems to have completed the task, and a DBF can now be made from a data table.

Once you have studied the other sub-sections from section 15, you may wish to do a similar task to the one above, but dump the table data into a CSV file format.

You will find all the ideas and working code snippets required throughout section 15 - all you need to do is think through the problem and assemble the parts in the correct order.

Well, you need to test your code thoroughly too :-)

Good Luck !!

PHH Feb. 2008


DBF to XML format

While we are on the subject of data files abd the well tried DBF format used by countess numbers of developers over the years, let us take the opportunity to look at changing the format of data stored in files, and also a look to the present and data formats for internet transmission.

I must confess I have been late to the XML world, mainly because I have not be customer driven in that direction, and secondly because I am not a tag script lover, I have to confess :-)

However, when I discovered just how easy it was to move data from DBF into XML format I thought it was time to "go for it".

If you look at section 25 on the DataGridView control, you will see that we need to construct data tables, which are then used by the control for display purposes. It was while looking through the SDK documentation that I noticed a few related methods which seemed interesting - and subsequently proved to be very useful indeed.

Lets name two of these here, WriteXML() and WriteXMLSchema().

Once we have a DataTable object which has been instantiated and filled with data from a DBF, it is extremely easy to copy all the table data into a pair of XML files on disk.

One file called the XMLSchema, has all the information about the fields used, and is therefore quite short, or small, in relation to the data file. The other is the data itself, in XML format.

Let us just see first of all, how easy this is once we have a data table in existence.

The code shown below is what I added to the "WarpGrid" demo app to allow data dumping in XML format. It simply relates to an extra push button and the click event for that particular button.

Firstly, the line of code needed in the SetMyEventHandlers() method is as follows :-

//----------------- push buttons for XML support -------------------

SELF:opbDumpToXMLfile:Click += EventHandler{ SELF , @pbDumpToXMLfileClick() }

Now we can see what the methods to handle the event, and to do the action when the event takes place, actually look like.

Take note that all the developer is doing really is supplying a pair of file names - nothing more. We are relying on the DataTable methods to do their job.

Here is the VERY useful code :-

METHOD pbDumpToXMLfileClick(o AS OBJECT , e AS EventArgs) AS VOID

//------- do action method to send table data to XML file(s) ---------

SELF:DumpData2XMLfile()

RETURN

METHOD DumpData2XMLfile() AS VOID

LOCAL XMLTable AS DataTable

XMLTable := oWarpSet:Tables[SELF:TableInFocus]

XMLTable:WriteXml("C:\VDNUK\xmlTestData.xml")

XMLTable:WriteXmlSchema("C:\VDNUK\xmlTestSchema.xml")

XMLTable := NULL_OBJECT

RETURN

So how about looking at parts of the files produced to see what has happened.

First lets view the schema :-

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

And now a records from the data file itself :-

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

So we appear to have a very easy way to turn data in a DataTable into a XML files on disk.

All we need now is to see how we can create a suitable DataTable from any DBF file we have.

If we first see how to write code for a general way for reading all fields with all records into a DataTable, we can always consider how to adapt this later to do just selected fields and certain records.

At this point it is also worth saying that there are many ways to do things in dotNet and when we get more experience at Vulcan syntax and dotNet approaches, we can see just what may be done with the DataSourceControl class. There is a method for DataTable called "Load()" which needs such an object.

However, for now let us walk before we try to run :-) The code below seems to work fine :-

METHOD CreateWarpTable2(cDBF AS STRING, cTableName AS STRING) AS VOID

RDDSETDEFAULT("DBFCDX")

Cursor.Current := Cursors.No

//-------------- display Hour Glass --------------

LOCAL oWD AS DBServer

LOCAL cFilePath, cText, cFieldName AS STRING

LOCAL WarpTableN AS DataTable

LOCAL oRow AS DataRow

LOCAL i, nFields, k, nLen, nStep AS LONG

LOCAL nStepCount, nRecs AS LONG

WarpTableN := DataTable{cTableName}

SELF:oWarpSet:Tables:Add(WarpTableN)

cFilePath := "c:\VDNUK\"+cDBF

oWD := DBServer{cFilePath}

oWD:GoTop()

nFields := oWD:FCount

FOR i := 1 UPTO nFields

cFieldName := oWD:FIELDNAME(i)

WarpTableN:Columns:Add( DataColumn{cFieldName} )

NEXT

nRecs := 0

SELF:MaxRow := -1

DO WHILE ! oWD:EOF

oRow := WarpTableN:NewRow()

FOR i := 1 UPTO nFields

cFieldName := oWD:FIELDNAME(i)

oRow[ cFieldName ] := oWD:FIELDGET(i)

NEXT

WarpTableN:Rows:Add(oRow)

SELF:MaxRow +=1

nRecs +=1

IF nRecs > 698

EXIT

ENDIF

oWD:Skip(+1)

ENDDO

oWD:Close()

oWD := NULL_OBJECT

Cursor.Current := SELF:Cursor

//------------------ display arrow -------------------

RETURN

When the code is run all 117 fields of the DBF are correctly processed, however, you will notice I added a section of code to limit the reading to only 698 records. This was simply to save time in testing and development, and has no other purpose.

I leave it in the displayed code as a reminder to you that this may be a helpful trick while doing the basic development, BUT, don't forget to remove it later !

It is probably time for you to look at the sub-section which allows XML data to be read directly into a table from a pair of XML disk files.

Alternatively, look at how to change DBF into CSV format - very useful in a Windows environment although we need to check out just what tools are already available, once we can write Vulcan syntax and get things working for ourselves.

In some ways what we are doing here is just good coding exercises to help us learn.

PHH Feb. 2008


XML to DBF format

Enter topic text here.


DBF to CSV format

I know that the DBServer class has a "CopyDelimited" method which seems to do what we are going to re-invent, BUT, for some reason, or reasons, in the past I had occasion to look-out a coded solution and tweak it. So we will study that coded solution here.

It will also prove useful to solve other related problems in this section, where we don't have a solution "out of the box".

Was it that I wanted a first line of my CSV file to be the field names so that Excel could use them ? I can't remember. Anyway it is good Vulcan coding practice.

Before we look at any dotNet ways to read or write CSV data into data tables, let us get some basic Vulcan syntax practice by rewriting an existing VO class called "CSVfromDBF".

The VO class actually produces a text file with the following string data at its start :-

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

There are actually 118 field names first, and then the start of the data records.

I will copy the code over from the VO class into Vulcan and get it to work pretty much as it is. However, once we have it working we can then make a few changes to improve things, for example to use StringBuilder for some, or most, of the string handling. This should improve performance without doing very much to the code.

Okay, I have done the above task after a week's break, and "finding" ZedGraph, however, the CSV job is completed and works fine.

Please note the original VO class came to me from Alwyn (AJB) so thanks for that - and I made a few minor changes at VO level, and now a few more at Vulcan level.

In converting the VO class I have already made some suitable changes to the code to make it more like Vulcan.NET should be, in the world of creating new code from scratch. You may wish to review the Full Working Code section to see what it now looks like.

I would suggest you notice the use of String Builder, Stream Writer, and the use of the "e" syntax for escape sequences. Look at how the string data has a single quote character replace any double quotes in the data, and then the whole data field enclosed in double quotes. This is all a bit new to VO converts - but easy to get used to.

Lets see what it looks like when opened in Excel :-

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

You will notice above, that 'The Bungalow' has single quotes - this has been a test for the swap routine as in the original data I made then doubles.

All the different data types available to DBF format have come over as required, like logic, numeric and dates.

However, the eagle eyed amongst you (like AJB) may notice the "y3 Swallowfield Rd" - well this is not a strange behaviour of my code, just the apparent incorrect data. "y3" is what it looks like in dBAdministrator.

I have a feeling that it may be "73" which is required ;-)

Note that this class does any DBF with any number of fields as it is at the moment. It also puts all field data in double quotes to make sure that the format definitely works under all conditions.

However, you will need to do the final checking and testing yourself.

You may also like to try out the field exclusion part which has been ignored so far. You may also feel ambitious and write a GUI with a form which displays all fields names in one list box and allows either copying by item clicking - or if you are very ambitious - by the drag and drop method shown in section 8 which Chris Pyrgas sent to me.

You have enough of a start with the enclosed code, it is up to you how you employ it and how tight you make it so as for example the CSV filename provided in the Constructor, is checked to see if ".CSV" id already included.

There are lots of little things you can do to help you get some basic Vulcan practice.

Don't forget "USING System.IO" for using StreamReader / Writer.

PHH Feb. 2008


CSV into Data Table

Although this seems a little old fashioned by today's SQL and XML standards, it may be a useful thing to do from time to time, and if nothing else it is just good practice for doing some basic Vulcan coding.

After all Excel still reads in CSV, and with field names too just like we are creating.

It is only by sitting down and getting your fingers onto the keyboard that you will start to feel at home with Vulcan, and the newer things that the syntax and dotNet has to offer.

Let us start by giving ourselves a "set task". We are to take the CSV file from the previous sub-section, and read it into a data table.

In these Click Start notes it is too early to start worrying about table schemas and data types. This can be dealt with at a later date. Lets just do the work (coding) so that we may display the CSV data as string data in a table which can be shown on screen.

Once we have the table, we can then display it using the WarpGridView control on the "Warp Form" we saw in the second of the sub-sections in this section (number 15).

First we need to open the file for "Stream Reading", and then read in the first line.

From this line we can then obtain all the field, or column names we need to have available to us. Once we have the names we may then decide not to use them all in our table, and only select which we want - as we did in the case of the DBF read to table.

Let us now look at this section of code below :-

METHOD OpenCSVstream()AS LOGIC

LOCAL lResult AS LOGIC

LOCAL cLine AS STRING

lResult := FALSE

oSR := StreamReader{SELF:CSVfullpath}

cLine := oSR:Readline():TRIM()

MessageBox.Show(cLine)

IF cLine:Length > 13

SELF:ColumnNameLine := cLine

lResult := TRUE

ENDIF

RETURN lResult

The above code open the CSV file and reads in the first line. Don't worry about the significance of 13 - its just an appropriate number, and my lucky number :o)

Once we have this first line stored in class variable (IVAR) "ColNameLine" this can then be used by the code below to find the individual column, or field names :-

METHOD FindFieldNames() AS LOGIC

LOCAL lResult AS LOGIC

LOCAL cLine, cLeft, cRight AS STRING

LOCAL nLen, nPosn, i, k AS DWORD

LOCAL aNames AS ARRAY

aNames := {}

lResult := TRUE

cLine := SELF:ColumnNameLine:TRIM()

nLen := cLine:Length

MessageBox.Show("line length :- "+nLen:ToString())

nPosn := cLine:IndexOf(",")

DO WHILE .T.

IF nPosn = -1

cLeft := cLine

AADD(aNames, cLeft)

EXIT

ENDIF

cLeft := cLine:Substring(0,nPosn)

AADD(aNames, cLeft)

cLine := cLine:Substring(nPosn+1)

nPosn := cLine:IndexOf(",")

IF nPosn = -1

cLeft := cLine

AADD(aNames, cLeft)

EXIT

ENDIF

ENDDO

//MessageBox.Show(cLeft)

//MessageBox.Show("length of names array :- "+ALen(aNames):ToString())

nLen := ALen(aFields)

SELF:ColNames := STRING[]{nLen}

FOR i := 1 UPTO nlen

k := aFields[i]

ColNames[i] := aNames[k]

NEXT

This is reasonably straight forward and simply shows code to identify the commas in the text line, in order to split the text at these comma positions. When no more commas, then we have the last text fragment - or column / field name.

I have left in place (commented out) code suitable for testing and checking. This is also true for the full working code section included in that sub-section.

Notice I am trying to use dotNet methods and properties whenever possible, and not VO functions - for example nLen:ToString() in place of NTrim(nLen). It seems most sensible to get used to these ASAP and life will get easier as a consequence.

If you view the rest of the working code it follows through without too much complexity, so lets finally look at the part shown below which is successfully reading in nearly 70 rows of data and putting it into table rows after having added the columns and column titles :-

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

You will notice I have restricted the fields to a much smaller number than the 118 in the file being read.

And, just to check, I have printed out the cell contents for row 69 and column ten. Yes the numbering seems to be both zero and one based - but it has to do with table structure - I just did it "al dente" !

(Some may translate this as "suck it and see" !)

Now we have a data table we can add this to a data set and select it as a data source for the DataGridView control.

I will leave this to you, having done the tedious part of all this. Just copy what was done for setting and selecting data sets and tables in the main Warp demo app - Section 25 in these Click Start notes. ( Changed my mind - its a bit tricky so I have included the code in the full working sub-section and there is a sample of output next to some of this code, right at the end of this section.

Good Luck - and don't forget to follow this up by seeing how to set schemas for the table so that data types may be chosen and applied.

However, for now, one step at a time.

Oh! I have just noticed from the above image, we have not removed the surrounding quotes. Although the DataGridView may handle this as ExCEl (but I doubt it) we ought to remove these double quotes - since we put them there in the first place when we made the CSV file from the original DBF.

To correct this I have just made the following method and tested it out - live output is for printing the field names of just the selected fields :-

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

I have included this method call in all the necessary places in our class so that both column names and column data cell contents have the extra surrounding double quotes removed by us.

And now the

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

Hope you can get the DataGridView control to show your CSV data ! At least from the small snippet of live screen output shown, you can see that the double quote removal seems to have worked Ok and that both column names and cell contents are seen as you would expect.

PHH Feb. 2008


CSV to DBF format

In fact this problem has already been solved - that is a relief :o)

If you use what we have already developed in this section you can read a CSV file into a data table, then dump it out to the DBF using the DataTable dump to DBF routine.

There, wasn't that easy !

You may be interested to know that in doing this the dump routine has given you back more information than you started with. You now have field widths which you did not have associated with the CSV file.

If you look at the XML schema file in sub-section two of this section, you will also notice that there is no data to say what the width of the field needs to be.

Simply this is due to trying to match up old with new technologies. It would appear that modern technology tries to accommodate the widest of data as long as it is of the type required.

Not so old DBFs - so we have to be flexible in both our approach and program coding.

PHH Feb. 2008


Data Table from XML

This sub-section is not too demanding as all we need to do is try and read back the pair of XML files we created in sub-section 2.

Once the XML data is in a table I will display it in the Warp Grid control on the demo app. All that I will do is add a push button to start the table creation and population - once the files have been located.

To develop and test the code to do the job as described, we need to know that to do it this simple way (shown below), we need to make the table with exactly the same name as when we had the table before the "dump" to XML files previously.

I am looking into the dotNet way to find the table name from the schema, or a way to supply a different name if only the file names are known.

For now I will use "PatPersonal" as the table name in the code. (View the schema in notepad to verify this.)

Lets see what some working code looks like :-

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

Once we look down this code for the "clever" bits you will see that in fact there are only two lines which do the "business" of opening and reading the file data.

The table methods "ReadXmlSchema()" and "ReadXml()" are these that do all the work.

The code lines to display the table data from the table set are much as before.

PHH Feb. 2008


Going Further - Data Types

So far in this section we have had data tables displayed in the DataGridView control which had all the columns of character, or STRING, data type. This was OK for getting used to the new dotNet way of doing things and learning some Vulcan syntax to help in the matter. However, if we want to move to a more useful and professional approach we need to preserve the data types from file to table.

In fact it is not as bad as you may first think, as dotNet makes life very easy for the developer, yet again !

Obviously, it is when we read in data from an external file that we need to first specify the data type. The following is what the SDK documentation has to say on this matter :-

An exception is generated when changing this property after the column has begun storing data.

If AutoIncrement is set to true before setting the DataType property, and you try to set the type to anything except an integer type, an exception is generated.

So simply put, this means we need to set the data type for each column (or allow the STRING default), before any data is read into the table.

It would also be very nice to be able to see the overall schema, and print it out, before dumping to whatever format - XML for example.

For now, let us take the example of reading in a DBF. This has a structure which can easily be found, and it has information in it which specifies the data type of each field in the file.

This is what the VO28 help notes has to say on the matter :-

Description :

An array containing the structure of this data server, whose length (that is, number of elements) is equal to the number of fields in the server.

Each sub-array is comprised of five elements, which contain the following information, in this order:

Constant

Attribute

DBS_NAME

cName

DBS_TYPE

cType

DBS_LEN

nLength

DBS_DEC

nDecimals

DBS_ALIAS

cAlias

So let us look at some basic Vulcan code which will find, and use this structure information, to apply a data type to each column - corresponding to the field in the DBF.

cFilePath := "c:\VDNUK\"+cDBF

oWD := DBServer{cFilePath}

oWD:GoTop()

LOCAL aDBF AS ARRAY

LOCAL cType AS STRING

LOCAL nDec AS DWORD

aDBF := oWD:DBStruct

nFields := ALen(aFields)

FOR i := 1 UPTO nFields

k := aFields[i]

cFieldName := oWD:FIELDNAME(k)

oDC := DataColumn{cFieldName}

cType := aDBF[k,2]

nDec := aDBF[k,4]

IF cType == "C"

oDC:DataType := System.TYPE.GetType("System.String")

ENDIF

IF cType == "L"

oDC:DataType := System.TYPE.GetType("System.Boolean")

ENDIF

IF cType == "D"

oDC:DataType := System.TYPE.GetType("System.DateTime")

ENDIF

IF cType == "N"

IF nDec = 0

oDC:DataType := System.TYPE.GetType("System.Int32")

ELSE

oDC:DataType := System.TYPE.GetType("System.Double")

ENDIF

ENDIF

WarpTableN:Columns:Add( oDC )

NEXT // i

So at this point we should have a column for each field and the data type is set as that related to the fields in the original DBF.

We now need to read in the data and fill the rows of the table. Let us see what the code looks like :-

nRecs := 0

SELF:MaxRow := -1

DO WHILE ! oWD:EOF

oRow := WarpTableN:NewRow()

FOR i := 1 UPTO nFields

k := aFields[i]

cFieldName := oWD:FIELDNAME(K)

oRow[ cFieldName ] := oWD:FIELDGET(K)

NEXT

WarpTableN:Rows:Add(oRow)

SELF:MaxRow +=1

//nRecs +=1

//IF nRecs > 698

//EXIT

//ENDIF

oWD:Skip(+1)

ENDDO

Note the code to increment the progress bar has been removed to keep this visually simple.

Remember the array aFields restricts the fields used in the table to only those values supplied.

Let us look at what results from such code, and see the live output below :-

You will notice that the logic field is now a column with a check box, and the character, or string, version of the unique reference number when sorted displays the exact pattern we would expect for a string sort Z to A.

When we choose to sort on the numeric form of the reference number we get the following :-

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

And if we sort on the Date of Birth column (DOB) by clicking the column header we get the following order :-

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

A visual inspection shows that it is a date sort and not a character sort - so our code must be working as required.

PHH Feb. 2008


General Conversion GUI Tool

Disk Data File Format - Multi Converter :

We are now in a position to construct a simple GUI applet which will serve as a good tool to convert data files from one disk file format into another.

All we need is a single Form on which we can then have a DataGridView control which can show our data tables which we construct.

Other form controls will allow us to select files to be read, from XML, CSV or DBF format. We will be able to select the source folder too using the Browse Folder dialog.

Lets look at the basic form :-

And the folder selection too :-

On selection of a data folder, this is displayed in the caption bar and the drop down lists filled with file names of the relevant type.

The above image shows the list of DBFs found in folder C:\VDNUK and that a few others have been added to the CSV, XML and XSD combo box list too.

Once we point at a disk file it becomes the file in focus. Pressing the "Load File in Focus" button causes it to to read it into a table and displayed.

We need to apply some of the classes and knowledge we have built up so far in this section of ClickStartVDN.

Here we have the table displayed in the DataViewGrid control :-

XML will read in easily, but, we will need a way of finding out what the table name is in the schema. However, the schema holds, and applies, all the information to set the data types on the columns needed to be used.

Our basic screen tool will read the whole of the file, that is, all fields and all records.

Reading DBF's is straight forward too as we have the column data types and the field names etc. etc.

Let us see what the "View Table Schema" push button facility looks like :-

Notice we have even got a facility to print the schema of the table as read in directly. This looks as follows :-

We can print to the default printer from the Print Preview dialog screen.

Where we really hit a problem is the CSV format file. There is no information stored to say what data types were in existence before the data was "dumped" as separated strings.

So we will have to add a facility to our screen form tool - we will allow the user to view the data and then select a data type for any column they choose.

As we run the the mouse over the columns the mouse enter event is used to display the data type and name of the column entered - this shows in red at the bottom right of the table.

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

To change the data type to be used in a new form of the same table (the data type of an existing table can't be changed once it has data), all we need do is double click the mouse in the column required for change.

Then a new small selection screen is displayed, allowing the user to select a suitable data type :-

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

Notice I have had sure we have a CSV file in focus as this will have all string fields, the example is where we are changing String to INT32 for the PATUNREF column.

Simply saving this will add it to the list of new data types to be applied to create the new table.

I will do a few others and then display the proposed schema by checking the box for "View Proposed Schema" which toggles between the existing table in view and the proposed list.

Here is what we get :-

Once the user is finished this "creative" process they will set into motion a process to make another data table with the proposed schema as soon as they choose to "Dump Table to File".

To create the dump table, the same data will be read into the new columns and rows, with the chosen data types set on the correct columns prior to data transfer.

Before a new table is made and dumped to disk file, the user may also wish to exclude some of the fields from this process.

Another small edit screen is displayed for the user to check boxes against the field names to be excluded see below :-

Having select a few columns or fields, the user can view these using the "View Excluded Fields" push button. A typical result is shown below :-

Most of the other controls are reasonably obvious, the user has to chose a format for dumping the data, and an extra button is provided for a direct data dump from DBF to CSV, once a DBF file has been selected.

This bypasses the creation and display of a data table, but of course the original field types, and all the columns, are preserved. To make any changes the user has to go through the table stage.

One final issue to take into account is the dumping of DateTime values. In order to make the DevFest deadline I will just convert to dates for all these.

XML is no problem as it can have such values. However, for DBF and CSV, this is probably best done by making a pair of values in the target file, one with date value, and the other with a time string value.

There are also a few issues with some dotNet numeric forms but let us see just how far we can go with what we have penciled in so far.

For DevFest I will include the code which does as demonstrated so far, even though there are a few issues to sort out further.

Please enjoy getting to grips with some of the code involved with this project, and I will try to add and test Paul Piko's class to check that the XML pair of files fro reading in are 100% compatible.

Thanks for this Paul :-)

You may also be interested to know that I used ClickStart notes to help prompt me with code and syntax along the way to a working app.

For the DevFest CD I will include the actual exe file and a folder of files for you to test - but don't forget to move this to a read / write HDD folder first !

PHH Feb.2008


Full Working Code

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

>>> DBF to CSV <<< don't forget the USING System.IO for Stream Reading / Writing !

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

USING System.IO

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

CLASS CSVfromDBF

EXPORT CSVfilename, CSVfolder AS STRING

EXPORT DBFfilename, DBFfolder AS STRING

EXPORT CSVfullpath, DBFfullpath AS STRING

EXPORT aExFieldNos, aExFieldNames AS ARRAY

EXPORT aExclude AS ARRAY

EXPORT lFilesFoldersOK AS LOGIC

EXPORT aMissing AS ARRAY

EXPORT oServerP4X AS DBServer

PROTECT aMonths AS ARRAY

CONSTRUCTOR(cFileCSV AS STRING, cFolderCSV AS STRING, cFileDBF AS STRING, cFolderDBF AS STRING)

SUPER()

RDDSETDEFAULT("DBFCDX")

cFileCSV := AllTrim(cFileCSV)

SELF:CSVfilename := cFileCSV

cFolderCSV := AllTrim(cFolderCSV)

SELF:CSVfolder := cFolderCSV

cFileDBF := AllTrim(cFileDBF)

SELF:DBFfilename := cFileDBF

cFolderDBF := AllTrim(cFolderDBF)

SELF:DBFfolder := cFolderDBF

SELF:lFilesFoldersOK := SELF:CheckFileFolders()

IF SELF:lFilesFoldersOK

//? "Both OK !"

ELSE

//? "at least one missing"

ENDIF

SELF:PostInit()

RETURN

METHOD PostInit() AS VOID

RDDSETDEFAULT("DBFCDX")

aMonths := {}

AAdd(aMonths, {"January"})

AAdd(aMonths, {"February"})

AAdd(aMonths, {"March"})

AAdd(aMonths, {"April"})

AAdd(aMonths, {"May"})

AAdd(aMonths, {"June"})

AAdd(aMonths, {"July"})

AAdd(aMonths, {"August"})

AAdd(aMonths, {"September"})

AAdd(aMonths, {"October"})

AAdd(aMonths, {"November"})

AAdd(aMonths, {"December"})

aExclude := {}

aExFieldNos := {}

aExFieldNames := {}

RETURN

METHOD AsCSVstring(uVal AS USUAL, cDType AS STRING ) AS STRING

LOCAL cRet, cDQ, cSQ AS STRING

LOCAL nMonth AS LONG

cDQ := e"\""

cSQ := e"\'"

DO CASE

CASE cDType == "C"

cRet := uVal

cRet := AllTrim(cRet)

cRet := StrTran(cRet, cDQ, cSQ) // Replace double with single quotes

cRet := cDQ+cRet+cDQ // Add quotes

CASE cDType == "D"

IF Empty(uVal)

cRet := ""

ELSE

nMonth := Month(uVal)

IF nMonth > 0 .and. nMonth < 13

cRet := PadL(Day(uVal),2,"0")+"-"+Left(aMonths[Month(uVal),1],3)+"-"+NTrim(Year(uVal)) // Format 01-Apr-2004 (Swedish/English)

ENDIF

ENDIF // 'Understood' by Excel to be a 'date'.

cRet := cDQ+cRet+cDQ // Add quotes

CASE cDType == "L"

cRet := IIF(uVal, "T", "F")

cRet := cDQ+cRet+cDQ // Add quotes

CASE cDType == "N"

cRet := NTrim(uVal)

cRet := cDQ+cRet+cDQ // Add quotes

OTHERWISE

ENDCASE

RETURN cRet

METHOD CheckFileFolders() AS LOGIC

LOCAL lResult AS LOGIC

LOCAL cFile AS STRING

lResult := TRUE

SELF:CSVfullpath := SELF:CSVfolder+SELF:CSVfilename

SELF:DBFfullpath := SELF:DBFfolder+SELF:DBFfilename

cFile := SELF:CSVfullpath+".CSV"

IF ! File(cFile)

lResult := FALSE

MessageBox.Show("Not found CSV file")

ELSE

MessageBox.Show("YES, found CSV file")

ENDIF

cFile := SELF:DBFfullpath+".DBF"

IF ! File(cFile)

lResult := FALSE

MessageBox.Show("Not found DBF file")

ELSE

MessageBox.Show("YES, found DBF file")

ENDIF

RETURN lResult

METHOD DoAllFieldsExist() AS LOGIC

LOCAL lResult AS LOGIC

LOCAL nCount, nField, i AS LONG

lResult := FALSE

IF ALen(SELF:aExclude) < 1

lResult := TRUE

RETURN lResult

ENDIF

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

nCount := oServerP4X:FCount

lResult := TRUE

FOR i := 1 UPTO ALen(SELF:aExclude)

nField := SELF:aExclude[i]

IF nField < 1 .or. nField > nCount

lResult := FALSE

AAdd(SELF:aMissing, nField)

ENDIF

NEXT // i

RETURN lResult

METHOD DoConvert() AS LOGIC

LOCAL amyStruct AS ARRAY

LOCAL i, nFields, nPosn, nLen2, nWritten AS LONG

LOCAL uContent AS USUAL

LOCAL cType, cName, cText, cFields, cR, cR2, cHead AS STRING

LOCAL lResult, lWriteOK, lFieldsOK AS LOGIC

LOCAL oSB AS StringBuilder

LOCAL oWD AS DBServer

lResult := FALSE

cR := CRLF

cR2 := cR+cR

//MessageBox.Show("Just before try to open DBF ....")

//MessageBox.Show("variable is :- "+SELF:DBFfullpath)

SetExclusive(TRUE)

oServerP4X := DBServer{SELF:DBFfullpath}

//MessageBox.Show("Just AFTER try to open DBF ....")

SetExclusive(FALSE)

IF ! oServerP4X:Used

RETURN lResult

ENDIF

//MessageBox.Show("open Okay ....")

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

lFieldsOK := TRUE

IF ALen(SELF:aExclude) > 0

lFieldsOK := SELF:DoAllFieldsExist()

ELSE

MessageBox.Show("exclude none ....")

ENDIF

IF ! lFieldsOK

cFields := SELF:ShowMissingFields()

cHead := "MISSING .... Fields to Exclude ! "

cText := cR+"At least one field in the 'Exclude Set' provided, does not exist !!! "+cR2+;

cFields+cR2+;

"This (these) will be ignored in the conversion. "+cR2

MessageBox.Show(cText, cHead)

ENDIF

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

amyStruct := oServerP4X:DBStruct

nFields := oServerP4X:FCount

//MessageBox.Show("number of fields :- "+NTrim(nFields))

oSB := StringBuilder{}

FOR i := 1 UPTO nFields //----------- could use a reduced set of field nos. ! ----------

nPosn := AScan(aExclude, {|x| x = i})

IF nPosn > 0

LOOP

ENDIF

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

cName := amyStruct[i,1]

cType := "C"

IF i < nFields

oSB:Append(SELF:AsCSVstring(cName, cType)+",")

ELSE

oSB:Append(SELF:AsCSVstring(cName, cType))

ENDIF

NEXT // i

oSB:Append(CRLF)

//MessageBox.Show(oSB:ToString())

LOCAL nCount AS DWORD

LOCAL cLine AS STRING

nCount := 0

oServerP4X:GoTop()

lWriteOK := TRUE

DO WHILE ! oServerP4X:eof

FOR i := 1 UPTO nFields //----------- could use a reduced set of field nos. ! ----------

nPosn := AScan(aExclude, {|x| x = i})

IF nPosn > 0

LOOP

ENDIF

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

uContent := oServerP4X:FIELDGET(i)

cType := amyStruct[i,2]

IF i < nFields

oSB:Append(SELF:AsCSVstring(uContent, cType)+",")

ELSE

oSB:Append(SELF:AsCSVstring(uContent, cType))

ENDIF

NEXT

oSB:Append(CRLF)

/*nCount +=1

IF nCount > 40

EXIT

ENDIF */ //---------- useful for testing ----------

oServerP4X:Skip(+1)

ENDDO

MessageBox.Show("Number of Records read :- "+nCount:ToString())

LOCAL oSW AS StreamWriter

oSW := StreamWriter{SELF:CSVfullpath+".csv", FALSE, Encoding.ASCII}

oSW:Write(oSB:ToString())

oSW:Close()

oSW:Dispose()

lResult := TRUE

//---------- could ope and StreamRead into another 0SB object and then compare ----------

//---------- TRUE if the same - FALSE if not -----------

RETURN lResult

METHOD ShowMissingFields() AS STRING

LOCAL cResult AS STRING

LOCAL i AS LONG

cResult := "/"

FOR i := 1 UPTO ALen(SELF:aMissing)

cResult += NTrim(SELF:aMissing[i])+"/"

NEXT // i

RETURN cResult

DESTRUCTOR()

oServerP4X:Close()

oServerP4X := NULL_OBJECT

RETURN

END CLASS

===================== End of DBF to CSV ======================

>>>>>>>>>>>>>>>>>>>>>>>>>>> CSV to Data Table >>>>>>>>>>>>>>>>>>>>>>

CLASS CSVtoDataTable

EXPORT CSVfilename, CSVfolder AS STRING

EXPORT CSVtableName AS STRING

EXPORT CSVfullpath,ColumnNameLine AS STRING

EXPORT CSVexists AS LOGIC

EXPORT oSR AS StreamReader

EXPORT ColNames, ColData AS STRING[]

EXPORT aFields AS ARRAY

EXPORT NumberOfFields, MaxRows AS DWORD

EXPORT CSVTable AS DataTable

EXPORT aFieldWidths AS ARRAY

EXPORT oDRC AS DataRowCollection

EXPORT oDR AS DataRow

EXPORT oDCC AS DataColumnCollection

CONSTRUCTOR(cFileCSV AS STRING, cFolderCSV AS STRING, cTableName AS STRING)

SUPER()

cFileCSV := AllTrim(cFileCSV)

SELF:CSVfilename := cFileCSV

cFolderCSV := AllTrim(cFolderCSV)

SELF:CSVfolder := cFolderCSV

cTableName := AllTrim(cTableName)

SELF:CSVtableName := cTableName

SELF:PostInit()

RETURN

METHOD PostInit()AS VOID

LOCAL cLine, cContent AS STRING

LOCAL i AS DWORD

//LOCAL oDRC AS DataRowCollection

//LOCAL oDR AS DataRow

aFields := {1,2,3,4,5,6,7,46,8,9,10,11,12,13,14,15,17,18}

SELF:CheckFileFolder()

IF SELF:CSVexists

IF SELF:OpenCSVstream()

SELF:FindFieldNames()

SELF:MakeDataTable()

FOR i := 1 UPTO 69

cLine := oSR:ReadLine()

IF cLine == NULL

EXIT

ENDIF

SELF:FindFieldData(cLine)

SELF:AddNewRow()

NEXT

cContent := "my testing ..."

oDRC := SELF:CSVTable:Rows

oDR := oDRC:Item[0]

cContent := oDR:ToString()

cContent := oDR:Item[10]:ToString()

//MessageBox.Show("field :- "+cContent)

SELF:MaxRows := oDRC:Count

//MessageBox.Show("row count :- "+SELF:MaxRows:ToString())

ENDIF

ENDIF

RETURN

METHOD CheckFileFolder()AS LOGIC

LOCAL lResult AS LOGIC

LOCAL cFile AS STRING

LOCAL nLen AS DWORD

lResult := TRUE

SELF:CSVfullpath := SELF:CSVfolder:Trim()+SELF:CSVfilename:Trim()

nLen := SELF:CSVfullpath:Length

IF !SELF:CSVfullpath:Substring(nLen-4):ToUpper() == ".CSV"

cFile := SELF:CSVfullpath+".CSV"

ENDIF

SELF:CSVexists := FALSE

IF ! File(cFile)

lResult := FALSE

MessageBox.Show("Not found CSV file")

ELSE

MessageBox.Show("YES, found CSV file")

SELF:CSVfullpath := cFile

SELF:CSVexists := TRUE

ENDIF

RETURN lResult

METHOD OpenCSVstream()AS LOGIC

LOCAL lResult AS LOGIC

LOCAL cLine AS STRING

lResult := FALSE

oSR := StreamReader{SELF:CSVfullpath}

cLine := oSR:Readline():Trim()

//MessageBox.Show(cLine)

IF cLine:Length > 13

SELF:ColumnNameLine := cLine

lResult := TRUE

ENDIF

RETURN lResult

METHOD FindFieldNames() AS LOGIC

LOCAL lResult AS LOGIC

LOCAL cLine, cLeft, cRight AS STRING

LOCAL nLen, i, k AS DWORD

LOCAL aNames AS ARRAY

LOCAL nPosn AS INT

LOCAL oSB AS StringBuilder

oSB := StringBuilder{}

aNames := {}

lResult := TRUE

cLine := SELF:ColumnNameLine:Trim()

nLen := cLine:Length

//MessageBox.Show("line length :- "+nlen:ToString())

nPosn := cLine:IndexOf(",")

DO WHILE .T.

IF nPosn = -1

cLeft := SELF:RemoveDQuotes(cLine)

AAdd(aNames, cLeft)

EXIT

ENDIF

cLeft := SELF:RemoveDQuotes(cLine:Substring(0,nPosn))

AAdd(aNames, cLeft)

cLine := cLine:Substring(nPosn+1)

nPosn := cLine:IndexOf(",")

IF nPosn = -1

cLeft := SELF:RemoveDQuotes(cLine)

AAdd(aNames, cLeft)

EXIT

ENDIF

ENDDO

MessageBox.Show(cLeft)

MessageBox.Show("length of names array :- "+ALen(aNames):ToString())

nLen := ALen(aFields)

SELF:ColNames := STRING[]{nLen}

SELF:NumberOfFields := nlen

FOR i := 1 UPTO nLen

k := aFields[i]

ColNames[i] := aNames[k]

oSB:Append(ColNames[i]+CRLF)

NEXT

MessageBox.Show(oSB:ToString(),"Column Names :")

/*LOCAL cText AS STRING

cText := ""

FOR i := 1 UPTO nLen

cText += SELF:ColNames[i]+CRLF

NEXT

MessageBox.Show(cText,"Column Names :") */

RETURN lResult

METHOD FindFieldData(cLine AS STRING) AS LOGIC

LOCAL lResult AS LOGIC

LOCAL cLeft, cRight AS STRING

LOCAL nLen, i, k AS DWORD

LOCAL aData AS ARRAY

LOCAL nPosn AS INT

aData := {}

lResult := TRUE

cLine := cLine:Trim()

nLen := cLine:Length

//MessageBox.Show("line length :- "+nlen:ToString())

nPosn := cLine:IndexOf(",")

DO WHILE .T.

IF nPosn = -1

cLeft := SELF:RemoveDQuotes(cLine)

AAdd(aData, cLeft)

EXIT

ENDIF

cLeft := SELF:RemoveDQuotes(cLine:Substring(0,nPosn))

AAdd(aData, cLeft)

cLine := cLine:Substring(nPosn+1)

nPosn := cLine:IndexOf(",")

IF nPosn = -1

cLeft := SELF:RemoveDQuotes(cLine)

AAdd(aData, cLeft)

EXIT

ENDIF

ENDDO

//MessageBox.Show(cLeft)

//MessageBox.Show("length of data array :- "+ALen(aData):ToString())

nLen := ALen(aFields)

SELF:ColData := STRING[]{nLen}

FOR i := 1 UPTO nLen

k := aFields[i]

ColData[i] := aData[k]

NEXT

/*LOCAL cText AS STRING

cText := ""

FOR i := 1 UPTO nLen

cText += SELF:ColData[i]+CRLF

NEXT

MessageBox.Show(cText,"Column Data :") */

RETURN lResult

METHOD RemoveDQuotes(cText AS STRING) AS STRING

LOCAL cDQ AS STRING

LOCAL nPosn AS INT

LOCAL nLen AS DWORD

cDQ := e"\""

cText := cText:Trim()

nPosn := cText:IndexOf(cDQ)

IF nPosn = -1

RETURN cText

ENDIF

IF nPosn = 0

cText := cText:Substring(1)

ENDIF

nPosn := cText:IndexOf(cDQ)

IF nPosn = -1

RETURN cText

ENDIF

nLen := cText:Length

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

RETURN cText

METHOD MakeDataTable() AS VOID

LOCAL nFields, i AS DWORD

LOCAL cFieldName AS STRING

SELF:CSVTable := DataTable{SELF:CSVtableName}

nFields := SELF:ColNames:Length

FOR i := 1 UPTO nFields

cFieldName := SELF:ColNames[i]

SELF:CSVTable:Columns:Add( DataColumn{cFieldName} )

NEXT // i

SELF:oDCC := SELF:CSVTable:Columns

RETURN

METHOD AddNewRow() AS VOID

LOCAL oRow AS DataRow

LOCAL nFields, i AS DWORD

LOCAL cFieldName AS STRING

nFields := SELF:ColData:Length

oRow := SELF:CSVTable:NewRow()

FOR i := 1 UPTO nFields

cFieldName := SELF:ColNames[i]

oRow[ cFieldName ] := SELF:ColData[i]

NEXT

SELF:CSVTable:Rows:Add(oRow)

RETURN

METHOD FindFieldWidths() AS VOID

IF !SELF:CSVexists

RETURN

ENDIF

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

LOCAL i, k, nWidth AS DWORD

LOCAL cContent, cText AS STRING

MessageBox.Show("Will find field widths for DBF ... ")

SELF:aFieldWidths := {}

FOR i := 1 UPTO SELF:NumberOfFields

AAdd(SELF:aFieldWidths, 0)

NEXT

FOR i := 0 UPTO SELF:MaxRows-1

oDR := oDRC:Item[i]

nWidth := 0

FOR k := 0 UPTO SELF:NumberOfFields-1

cContent := oDR:Item[k]:ToString():Trim()

nWidth := cContent:Length

IF SELF:aFieldWidths[k+1] < nWidth

SELF:aFieldWidths[k+1] := nWidth

ENDIF

NEXT

NEXT

cText := ""

FOR i := 1 UPTO SELF:NumberOfFields

cText += NTrim(SELF:aFieldWidths[i])+CRLF

NEXT

MessageBox.Show(cText, "Max Widths :- ")

RETURN

METHOD FindColumnNames() AS VOID

LOCAL oDC AS DataColumn

LOCAL cName, cText AS STRING

LOCAL i AS DWORD

cText := ""

FOR i := 0 UPTO SELF:NumberOfFields-1

oDC := SELF:oDCC:Item[i]

cText += oDC:ColumnName+CRLF

NEXT

MessageBox.Show(cText,"Table Column Names : ")

RETURN

METHOD CreateDBFstructure() AS VOID

LOCAL aDBF AS ARRAY

LOCAL nWide, i AS DWORD

LOCAL oDC AS DataColumn

LOCAL cName AS STRING

aDBF := {}

FOR i := 1 UPTO SELF:NumberOfFields

nWide := SELF:aFieldWidths[i]

oDC := SELF:oDCC:Item[i-1]

cName := oDC:ColumnName

AAdd(aDBF, {cName, "C", nWide, 0})

NEXT

DBCreate("C:\VDNUK\dump2dbf",aDBF,"DBFCDX")

Application.DoEvents()

RETURN

METHOD OpenAndDumpData() AS VOID

RDDSETDEFAULT("DBFCDX")

LOCAL oODD AS DBServer

LOCAL cFile, cFilePath, cDBF, cContent AS STRING

LOCAL i, k AS DWORD

cDBF := "dump2dbf"

cFilePath := "c:\VDNUK\"+cDBF

IF File.Exists(cFilePath)

MessageBox.Show("File found ...")

ELSE

MessageBox.Show("File NOT found !!!")

ENDIF

oODD := DBServer{cFilePath}

IF oODD:Used

MessageBox.Show("Open for use ...")

ELSE

MessageBox.Show("NOT Open for use !!!")

ENDIF

oODD:GoTop()

FOR i := 0 UPTO SELF:MaxRows-1

oDR := oDRC:Item[i]

oODD:Append()

FOR k := 0 UPTO SELF:NumberOfFields-1

cContent := oDR:Item[k]:ToString():Trim()

oODD:FIELDPUT(k+1, cContent)

NEXT

NEXT

oODD:Commit()

oODD:Close()

oODD := NULL_OBJECT

RETURN

DESTRUCTOR

SELF:CSVTable:Dispose()

SELF:CSVTable := NULL_OBJECT

oSR:Close()

oSR:Dispose()

oSR := NULL_OBJECT

RETURN

END CLASS

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

>>>>>>>>>> code to add to WarpGrid app >>>>>>>>>>>>>

METHOD pbReadCSVfileClick(o AS OBJECT , e AS EventArgs) AS VOID

SELF:ReadAndShowCSVdata()

RETURN

METHOD ReadAndShowCSVdata() AS VOID

LOCAL cTableName AS STRING

LOCAL oC2DT AS CSVtoDataTable

oC2DT := CSVtoDataTable{"MyCSVtest", "C:\VDNUK\", "MyCSVTable"}

cTableName := oC2DT:CSVtableName

MessageBox.Show(cTableName)

SELF:oWarpSet:Tables:Add(oC2DT:CSVTable)

Application.DoEvents()

SELF:TableCheck(cTableName)

oWarpGridView:Hide()

oWarpGridView:DataSource := SELF:oWarpSet:Tables[cTableName]

oWarpGridView:Show()

Application.DoEvents()

//oC2DT := NULL_OBJECT

RETURN

==================== Modified Warp Grid app Start =========================

FUNCTION Start( asCmdLine AS STRING[] ) AS INT

LOCAL nExitCode AS INT

LOCAL oForm AS WarpForm

//LOCAL oCSV AS CSVfromDBF

LOCAL oC2DT AS CSVtoDataTable

//oCSV := CSVfromDBF{"MyCSVtest", "C:\VDNUK\", "patanon32", "C:\VDNUK\"}

//oCSV:DoConvert()

//oCSV := NULL_OBJECT

oC2DT := CSVtoDataTable{"MyCSVtest", "C:\VDNUK\", "MyCSVTable"}

oC2DT:FindFieldWidths()

oC2DT:FindColumnNames()

oC2DT:CreateDBFstructure()

Application.DoEvents()

System.Threading.Thread.Sleep(1300)

Application.DoEvents()

System.Threading.Thread.Sleep(1300)

oC2DT:OpenAndDumpData()

Application.DoEvents()

System.Threading.Thread.Sleep(1300)

oC2DT := NULL_OBJECT

Application.EnableVisualStyles()

Application.DoEvents()

oForm := WarpForm{}

Application.Run(oForm)

RETURN nExitCode

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

>>>>>>>>>>>>> Read from XML files >>>>>>>>>>>>>>>

METHOD pbReadFromXMLClick(o AS OBJECT , e AS EventArgs) AS VOID

SELF:DoReadFromXML()

RETURN

METHOD DoReadFromXML() AS VOID

LOCAL cTableName, cFile, cHead AS STRING

LOCAL oXDT AS DataTable

cFile := "C:\VDNUK\xmlTestSchema.xml"

cTableName := "PatPersonal"

cHead := "Schema Search :"

IF file.exists(cFile)

MessageBox.Show("found :O)", cHead)

ELSE

MessageBox.Show("NOT found :o(", cHead)

RETURN

ENDIF

oXDT := DataTable{cTableName}

oXDT:ReadXmlSchema(cFile)

cFile := "C:\VDNUK\xmlTestData.xml"

oXDT:ReadXml(cFile)

IF !File.Exists(cFile)

oXDT:Dispose()

oXDT := NULL_OBJECT

RETURN

ENDIF

SELF:oWarpSet:Tables:Add(oXDT)

MessageBox.Show(cTableName,"Made and is Set !")

IF SELF:TableCheck(cTableName)

oWarpGridView:Hide()

oWarpGridView:DataSource := SELF:oWarpSet:Tables[cTableName]

oWarpGridView:Show()

ENDIF

RETURN

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

PHH Feb. 2008


Code for Multi Converter

Enter topic text here.


Section 16 - Drives, Folders, and Files

When you have studied this section and the ones on the GUI controls try the suggested exercise below :-

Suggested Exercise :- { how good are you ? }

Maybe as an exercise you would like to create a form with a list of available drives on the PC - these would be put in a listbox or combobox.

And then, after selecting one from the list, you display the folder and file structure in a Tree View control.

To extend this exercise, provide a list in a drop down of all the possible file types you can think of. Then allow these to be selected and put into a growing listbox control.

When you apply these selected formats, the TreeView will be re-drawn ignoring all files which do not have types in the selection.

Types should be able to be removed by clicking them in the listbox showing selected types.

If you get really good you may also like to add that a mouse right click sorts the list A-Z .

Good Luck :-)

PHH December 2007.


Drives

The 'DriveInfo' class in System.IO namespace is what is required for us to do all (or most common) things related to "drives".

The static method 'GetDrives' allows us to grab information about all the logical drives on the PC.

Below is a working example of this on my development PC :-

Now that we have a dotNet array of all the ten drives available at this PC we can now select individual drives from the list and gain lots more information about them.

Let us see if we can find, name, type, format, size and much more .... using some of the properties of the DriveInfo class.

Be careful, as I have just wasted quite a few minutes trying to use a loop to print out such information for every drive in turn :o(

If you don't do a test to see if the drive is ready before accessing further information then a run time error occurs.

The following code and example works however :-

You may be interested to know the members of the DriveType enumeration as this allows you an easy way to separate out the types so that such errors can be minimised by your logic and coding.

Now that you have the basic ideas you need to do your own work to get a feel for these tools.

Try writing a a section of code to separate out the drive types into fixed, removable, and CDRom - and put these in a set of combo boxes.

Good Luck !

PHH December 2007.


Folders - Directories

The Directory class in the System.IO namespace provides you with a wide range of useful tools.

It provides (exposes) static methods for creating, moving, and enumerating through folders and sub-folders.

I suppose the name shows its age :-) I prefer to use folder in place of directory these days.

In our old VO way of thinking these are a set of functions grouped together under a heading of "Directory". However, I like the fact that the syntax of you coding still feel moderm.

Let us take a short time to look at some useful methods.

You will need to study the Framework 2.0 SDK documentation to get full value for your money on this one but here is a snap shot and then a few more useful bits in Vulcan syntax.

I will leave you to test out how to create and delete, let us now look at the 'GetFiles' method.

Suppose we wish to find all the files in the "C:\VDNUK\" folder :-

However, we can do a lot more than this as we can also provide a search pattern for another overload to use. Let us look for all files beginning with "V" by using the following code :-

Now what about the number of EXE files in this folder ? Let us extend our search pattern technique :-

Finally, let us search for any file which is either a .DLL or a .DBF and group these into an array.

This list is made up from 19 dll's and 7 dbf's - you will have to take my word for it, and do your own testing to get a feel for this way of finding groups of files in folders.

You also need to look up how to extend searches to sub-directories too. Useful to know these things.

If you need a suggested task, write a section of code to find all the PRG files on drive C:

When I tried it I got a runtime error, so obviously there is something else I need to know - add it to the LONG list :O(

Good Searching :-)

PHH December 2007.


Files

The 'File' class provides a full set of 'static' methods for handling files. It can be thought of as a group of functions, in VO parlance, to deal with files.

You will need to experiment with these, and the 'FileInfo' class below, so you get both what you wish, and what suits your style of coding.

If you need to use more than just the odd static method, it may be better to instantiate a 'FileInfo' object as the file path does not then need to be supplied each time.

I often use the "File.Exists" static method, and don't feel bad about it either ;-) See below for an example of both in use.

The 'FileInfo' class provides instance methods for the creation, copying, moving and opening of files.

Now that we know how to find and classify all the drives on our PC, as well as find groups of specified files, we need to look at what we may find out about the files themselves.

First of all lets take a brief look at the properties of a specified file, if it exists.

In my development folder 'C:\VDNUK\' I have a file called 'VulcanRT.dll' and we will use this for testing out the syntax and usefulness of the 'FileInfo' class.

Notice how we can use the static method of the File class - 'on the fly' as it were. This is quite handy, however, I think we should use some of the other static methods of the File class sparingly - or in meaningful places in our code.

It is more desirable I feel to use the FileInfo class with its methods - generally - than resorting to VO style function use :-)

Anyway, I will leave it for you to make your own decisions on this, and to look deeper into the available properties. If I were you I would certainly look further into the file attributes enumeration. Study the SDK documentation for Framework 2.0 on this subject.

Now let us move on to the methods on offer in this 'FileInfo' class.

There seem to be a number which 'link' with StreamReader and StreamWriter - these are AppendText, CreateText, OpenRead, and OpenWrite.

My personal preference is to instantiate my own Stream objects. However, there are methods relating to security which may interest you. It all depends on your own interests and needs.

You may find the 'MoveTo' method useful as indeed the 'Equals'.

Good experimenting ! And, remember to move away from Clipper style function coding !!

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

Here is a useful example of the 'FileInfo' class in action !

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

Above I am getting the application (EXE file) full path and instantiating a 'FileInfo' object using the full path.

As you can see the Creation time is an available property, from which we can then print the short time and date.

This was done after a good suggestion from Peter Guetens when he did some initial testing on the ExaminePrinters tool.

Thanks Peter :-)

PHH December 2007.


System Folders

Users of Visual Objects may be familiar with the function "GetFolderPath" which returns a string for the system path specified.

This still works in Vulcan as you would expect it to, however there are dotNet alternatives, as will be shown below.

(At the time of writing this, version 1.0 of Vulcan returns an empty string for one or two of those it shouldn't. This will be corrected in time - thanks to Danilo for spotting the problem.)

So for example GetFolderPath(CSIDL_PERSONAL) on this computer gives a result of :- "C:\Users\BESTadmin\Documents" (My Documents)

The same result can be obtained using GetFolderPath(0x0005). So the developer provides a constant or an integer that represents the path.

So the folder "My Music" is found using constant "CSDIL_MYMUSIC" or integer 0x000d and the live result is :- "C:\Users\BESTadmin\Music".

Other possible values are shown below (from the VO28 SDK) :-

CSIDL_PERSONAL := 0x0005 // My Documents

CSIDL_MYMUSIC := 0x000d // "My Music" folder

CSIDL_APPDATA := 0x001A // Application Data, new for NT4

CSIDL_LOCAL_APPDATA := 0x001C // non roaming, user\Local Settings\Application Data

CSIDL_INTERNET_CACHE := 0x0020

CSIDL_COOKIES := 0x0021

CSIDL_HISTORY := 0x0022

CSIDL_COMMON_APPDATA := 0x0023 // All Users\Application Data

CSIDL_WINDOWS := 0x0024 // GetWindowsDirectory()

CSIDL_SYSTEM := 0x0025 // GetSystemDirectory()

CSIDL_PROGRAM_FILES := 0x0026 // C:\Program Files

CSIDL_MYPICTURES := 0x0027 // My Pictures, new for Win2K

CSIDL_PROGRAM_FILES_COMMON := 0x002b // C:\Program Files\Common

CSIDL_COMMON_DOCUMENTS := 0x002e // All Users\Documents

CSIDL_RESOURCES := 0x0038 // %windir%\Resources\, For theme and other windows resources.

CSIDL_RESOURCES_LOCALIZED := 0x0039 // %windir%\Resources\<LangID>, for theme and other windows specific resources.

CSIDL_COMMON_ADMINTOOLS := 0x002f // All Users\Start Menu\Programs\Administrative Tools

CSIDL_ADMINTOOLS := 0x0030

The class to be used from the dotNet Framework is the "System.Environment" class. It has lots of very useful properties and methods and is well worth a careful study!

A couple of things to note are :- finding the OS version, the machine name, the number of CPUs, finding all the logical drives, and particularly the static method GetFolderPath.

Environment variables may be set and retrieved with static methods too. And the 'command line' for the process may be acquired for use.

Let us now just focus on the static method "GetFolderPath". Remember that a static method is one which can be used from a class without prior instantiation.

So the static method "System.Environment(Environment.SpecialFolder.System)" returns the system folder as the parameter is the enumeration for required for "system".

There are many others in the "Environment.SpecialFolder" enumeration :-

ApplicationData The directory that serves as a common repository for application-specific data for the current roaming user.

A roaming user works on more than one computer on a network. A roaming user's profile is kept on a server on the network and is loaded onto a system when the user logs on.

CommonApplicationData The directory that serves as a common repository for application-specific data that is used by all users.

CommonProgramFiles The directory for components that are shared across applications.

Cookies The directory that serves as a common repository for Internet cookies.

Desktop The logical Desktop rather than the physical file system location.

DesktopDirectory The directory used to physically store file objects on the desktop.

Do not confuse this directory with the desktop folder itself, which is a virtual folder.

Favorites The directory that serves as a common repository for the user's favorite items.

History The directory that serves as a common repository for Internet history items.

InternetCache The directory that serves as a common repository for temporary Internet files.

LocalApplicationData The directory that serves as a common repository for application-specific data that is used by the current, non-roaming user.

MyComputer The "My Computer" folder.

{The MyComputer constant always yields the empty string ("") because no path is defined for the My Computer folder.}

MyDocuments The "My Documents" folder.

MyMusic The "My Music" folder.

MyPictures The "My Pictures" folder.

Personal The directory that serves as a common repository for documents.

ProgramFiles The program files directory.

Recent The directory that contains the user's most recently used documents.

SendTo The directory that contains the Send To menu items.

StartMenu The directory that contains the Start menu items.

Startup The directory that corresponds to the user's Startup program group.

The system starts these programs whenever a user logs on or starts Windows NT or later, or starts Windows 98.

System The System directory.

Templates The directory that serves as a common repository for document templates.

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

I suggest you try out some of these to see what your system gives.

Note to get current directory the static property "CurrentDirectory" does the trick, so :-

System.Environment.CurrentDirectory will return the required string.

This is also true for system directory :-

System.Environment.SystemDirectory will return the required string.

Obviously, if you have a "USING System" line at the head of you code then you can drop the 'system' before Environment.

Finally on this class, look at the static method "GetCommandLineArgs" which returns a string array containing the command line arguments for the current process.

Good luck with your testing !

PHH Jan. 2008


Section 17 - Formatting Drives

A little while ago I asked VOPS members via the Vulcan "group" how to use the shell function called SHFormatDrive() from a Vulcan app.

Well after a few very useful contributions from "senior colleagues" (senior by knowledge NOT age!), I have an answer which may interest quite a few potential Vulcan pioneers :-)

In order to call the function you need the line :-

_DLL FUNCTION SHFormatDrive(hwnd AS PTR, drive AS DWORD, fmtID AS DWORD, options AS DWORD) AS DWORD PASCAL:SHELL32.SHFormatDrive

adding to the existing PRG file, and the compiler option set to "Allow Unsafe Code".

Then the function calling code as below :-

LOCAL dwResult AS DWORD

dwResult := SHFormatDrive(SELF:Handle, dwdrive, 0, 0)

This allows access to all removable drives including USB pen drives and camera cards, as well as the infamous floppy disk Ginny :-)

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

It would appear that although many informed folk think the floppy is past-it, the same code is needed to format its replacement ! (Who said anything about progress ?)

I would imagine most APIs can be used, taken from VO in a similar manner.

Thanks must go to Paul "P", Chris Pyrgas, Phil McG & John Parker - for supplying info to stop me pestering them. Well done !!!

Exit type code also works :- (see attached ...)

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

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

//dwRet := SHFormatDrive( SELF:Handle, 9, SHFMT_ID_DEFAULT, 0 ) //------- drive J: --------

//------- use DriveInfo to allow only removables ---------

//------- second parameter is the drive number ------- zero is A -----------

DO CASE

     CASE dwRet == SHFMT_ERROR

          MessageBox.Show(SELF, "Error on last format","Result :" )

     CASE dwRet == SHFMT_CANCEL

          MessageBox.Show(SELF, "Last format was canceled","Result :" )

     CASE dwRet == SHFMT_NOFORMAT

          MessageBox.Show(SELF, "Drive is not formatable","Result :" )

     OTHERWISE

          MessageBox.Show(SELF, "Seems to have gone OK !","Result :" )

ENDCASE

RETURN

Also just after the USING statements put :-

#DEFINE SHFMT_ID_DEFAULT   0xFFFF

#DEFINE SHFMT_OPT_FULL     0x0001

#DEFINE SHFMT_OPT_SYSONLY  0x0002

#DEFINE SHFMT_ERROR     0xFFFFFFFFL

#DEFINE SHFMT_CANCEL    0xFFFFFFFEL

#DEFINE SHFMT_NOFORMAT  0xFFFFFFFDL

Finally, I would recommend tidying this up and using the DriveInfo class in .NET to access the list of removable drives. Don't forget that A: is zero so don't lose it when you trap the user input ;-)

Hope this helps someone !

In the near future I will attempt to included a couple of useful images to clarify, and show this in action.


Section 18 - Date, Time, and Related

As soon as you possibly can, move away from using the Date and Time that we all used so much in VO.

Get right into using the dotNet class of DateTime in the System namespace - assembly mscorlib.dll - as there is much to be gained.

The date and time is apparently stored in one entity, and has some powerful methods to help improve your coding.

It is also necessary to study TimeSpan while you are focusing your attention on DateTime, so look at that section just after those dealing with how to use date and time in projects.

Make sure in your testing and trial apps you have :-

#USING System

at the head of your PRG file, before any class declarations.

Because a lot of the working code is shown 'live' as it is running in the VIDE, with associated output by its side, you need to refer to the related section called 'Full working code' in order to be able to cut and paste into your own VIDE to make working examples for yourself.

As a final suggestion, why not look at the section on Arrays - so that you see how to have an array of DateTime 'entities' and TimeSpan ones too.

The Array section will also deal with sorting and searching these object types.

Finally, before we go to enjoy ourselves making demo tests and apps, here is a useful tip :-

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

The DateTime class (don't get mixed up with the structure we will use in the next section) has a Parse() method which is quite handy.

I could see this coming into its own when converting DBF type data into suitable dotNet format.

By doing the maths "fiddle" we are checking that all three DateTime variables are equal.

Below we can extend this so that we add in a time string in the format generated by VO and which I regularly hold in my DBF data fields.

This does look useful for conversions !

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

This may wee come into it own when we push the File Multi Converter to its limits in Section 15. We could combine a data and time field to make one DateTime field which could then be saved into an XML file format.

Discussion Regarding NULL Values and DateTime types :

This point of discussion can appear to get very confusing when you start to read various articles around the web. However, I feel it is possible to find a simple way forward which should work satisfactorily in the future with Vulcan and DotNet - even converting data from DBF fields.

The DateTime type has to have a value, it's a must. However, there are a couple of static fields called MinValue and MaxValue which come to the rescue.

The value of this constant is equivalent to 00:00:00.0000000, January 1, 0001.

The value of this constant is equivalent to 23:59:59.9999999, December 31, 9999, exactly one 100-nanosecond tick before 00:00:00, January 1, 10000.

In place of what we could put in a DBF date field "--/--/----" and called blank by many, we could use a MinValue. I will use this to denote that the DateTime field has been "set" by the system (initialized) but not filled. I will then use the MaxValue to show that an existing, or valid date has been present, but has been removed or over-written for some reason. So at least differentiation can then exist and be recognised.

There may be other suitable ways to proceed with certain form controls which handle DateTime types, but this seems as though it would do the job Okay for now.

PHH late 2007.


General use date - DateTime

This is not by any means an exhaustive exploration of the DateTime class, just some useful pointers to its use and flexibility.

Lets gets straight to business by seeing the DateTime class in action.

LOCAL oDT AS DateTime

oDT := DateTime{2008,03,13}

This will set the date to the 13th. March 2008. The first day of DevFest when this Help & Manual presentation needs to be complete.

Let us now see how to access and print some useful related information. What happens if we get a simple ToString value to print ?

Please note that the '@@' is required before 'Date' because it would be taken as the VO "DATE" data type and the inserted token would stop the correct compilation. Visually you can see this happen as the text for 'Date' is forced to upper case and colour red. The inclusion of the '@@' forces the compilation process to use the text as entered and not convert it to what it thinks it ought to be.

Let us now try to produce more useful output from this date.

We seem to be getting somewhere so lets revisit the date and see if we can print this out in a better format.

The image below shows what is on offer in the intellisense drop down. As well as the universal ToString() method we have a few more alternatives on offer.

You need to get into the habit of letting the intellisense do the hard work for you by suggesting what is available.

Let us try and change the code to allow us to print the date in some of the various styles on offer.

So that you get used to looking at the Framework 2.0 SDK documentation I will leave it up to you to find out more about the third line in the code above.

Now that we are getting a feel for the date part of the DateTime 'entity' - let us try some calculations based around dates.

We can add a number of days - or even a negative number of days - as well as using another DateTime object :-

Let us see how to use the static method of DateTime called 'Now()' to find the DateTime 'entity' for today. We will then add 110 days to this to check that this is how long I have to finish the 'Click Start VDN' notes.

If you are interested in date and time differences, then see the later section called 'General use - TimeSpan'

This should be enough to get you going on dates, and don't forget to consult the Framework 2.0 SDK documentation for more possibilities.

Let us now move on to the next section 'General use time - DateTime' to see how to handle times in some necessary ways.


General use time - DateTime

It's time to handle TIME !

Probably my experience with recording times in data files is similar to that of other developers using VO. I usually was just interested in the time of day at which data was recorded, just to help "finger print" things for tracking at a later stage - if necessary.

However, time is really a "point in time", and as such ought to be defined with year, month and day, as well as the obvious hour, minute and second. And, even calendar, but we won't go into that here.

The old way I did things meant I was very limited to what I could actually do with my recorded times. Without a lot of conversion and string handling etc. etc. I will start to use DataTime from now on even though DBF use causes a problem by its limited data types - this may signal a time to move away from DBFs too :-)

Two points in time appear on the time scale, and are separated by a time difference, or in dotNet parlance a TimeSpan.

Two times may be subtracted to find a time span, or difference. Summing time spans creates a resultant time span. Summing a DateTime and a time span creates a resultant DateTime, as it is a point in time "shifted" along the time scale axis by the amount of the time difference, or time span.

It is therefore more flexible for me to change my way of recording the time a user did a data Back-Up, or similar business event, by recording a DateTime value. I can then very easily find the time difference between two data back-up events. Or how long it was since the last one.

The DateTime structure handles two things which we as users in the past thought of as separate things, the time and the date.

By having a unified approach to date and time, time differences, or TimeSpan data types, can be more easily and effectively handled. Also it is scientifically correct this way.

However, there are still times when it is convenient to have the time and date as separate entities - so we need to get good at converting between the two forms as well as dealing with time spans too.

When we talk of time in the old way we mean "time of the day", which is not really a time at all - but a time difference from the time of midnight of the previous day. I suspect like me you are a slightly bit confused - BUT - lets put things to rights by looking at some real Vulcan code which handles common situations and events.

If you read the SDK documentation it can all seem a bit over the top, and confusing. The problem is however, that in the global world we inhabit, time is a very complex issue if it is to be of use to all people for all occasions. We just have to find a way to use the bits we require - in the correct way - without making errors.

To simplify things for most of us, for most of the time, I will stick to using "local" time. (Sorry Paul from down under !)

First let us remind ourselves of the possible parameters to use in the overload which takes date and time parameters - or should we correctly now say 'datetime' parameters :-

We have DateTime{year, month, day, hour, minute, second} OR DateTime{year, month, day} for just specifying a date.

Parameters

year

The year (1 through 9999).

month

The month (1 through 12).

day

The day (1 through the number of days in month).

hour

The hours (0 through 23).

minute

The minutes (0 through 59).

second

The seconds (0 through 59).

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

Okay, very good, we have a time of just over an hour past noon (lunch time) on day one ! But, I suspect this is not going to be much good.

Let us now take one of our 'down to earth' examples to make things easier.

Let us consider a few files in a folder on our hard drive. We are going to find these files and then have their last modified times read into an array. So the array will need to be one of DateTime, or of objects which have DateTime properties.

This is what the SDK has to say about a file time :-

A Windows file time is a 64-bit value that represents the number of 100-nanosecond intervals that have elapsed since 12:00 midnight, January 1, 1601 A.D. (C.E.) Coordinated Universal Time (UTC). Windows uses a file time to record when an application creates, accesses, or writes to a file.

The fileTime parameter specifies a file time expressed in 100-nanosecond ticks.

However, it looks as though this refers to older times, as the FileInfo class passes us a DateTime data type directly when we use the LastWriteTime property for example.

This is also true for the CreationTime property. And, remember we are keeping away from properties and methods with "Utc" on the end - as these are NOT for local time.

So things are looking simpler already. If we stick to using the very flexible FileInfo class (see Section 16 sub-section 3) the we are dealing directly in DateTime types.

Okay then, let us set ourselves a straight forward task of pointing at a folder, and reading in the filenames - with creation times and last write times (sometimes know as modify times) - and putting these into an array list of objects. We will call our simple new class "FileTimes".

We will also use some of our new found ideas (Section 2 latter parts) for using dynamic arraylists and converting them to arrays when necessary later.

First of all lets look at the class

CLASS FileTimes

EXPORT Name AS STRING

EXPORT CreationTime AS DateTime

EXPORT LastWriteTime AS DateTime

EXPORT FridayFile AS Boolean

EXPORT LastYearFile AS Boolean

EXPORT ThisYearFile AS Boolean

EXPORT DecemberFile AS Boolean

EXPORT MorningFile AS Boolean

EXPORT LateNightFile AS Boolean

EXPORT HowOldAsOfNow AS TimeSpan

EXPORT FromCreationToUpdate AS TimeSpan

EXPORT SinceLastUpdate AS TimeSpan

PROTECT Now AS DateTime

PROTECT ThisYear AS DWORD

PROTECT ThisMonth AS DWORD

CONSTRUCTOR(cName AS STRING, dtCreation AS DateTime, dtModify AS DateTime)

SUPER()

SELF:Name := cName:TRIM()

SELF:CreationTime := dtCreation

SELF:LastWriteTime := dtModify

SELF:Initialize()

RETURN

As you can clearly see, the objects of this class are instantiated with three input parameters, the file name, it's creation DateTime value, and it's last-write DateTime value.

The Constructor just makes the assignment to the class variables of suitable names.

Now let us see what the Initialize() method does :-

METHOD Initialize() AS VOID

SELF:Now := DateTime.Now

SELF:ThisMonth := SELF:Now:MONTH

SELF:ThisYear := SELF:Now:YEAR

LOCAL oDTW AS DayOfWeek

oDTW := DayOfWeek.Friday

IF SELF:CreationTime:DayOfWeek == oDTW

SELF:FridayFile := TRUE

ENDIF

IF SELF:CreationTime:MONTH == 12

SELF:DecemberFile := TRUE

ELSE

SELF:DecemberFile := FALSE

ENDIF

IF SELF:CreationTime:YEAR == SELF:ThisYear

SELF:ThisYearFile := TRUE

ELSE

SELF:ThisYearFile := FALSE

ENDIF

IF SELF:CreationTime:YEAR == SELF:ThisYear-1

SELF:LastYearFile := TRUE

ELSE

SELF:LastYearFile := FALSE

ENDIF

SELF:HowOldAsOfNow := SELF:Now - SELF:CreationTime

SELF:FromCreationToUpdate := SELF:LastWriteTime - SELF:CreationTime

SELF:SinceLastUpdate := SELF:Now - SELF:LastWriteTime

SELF:LateNightFile := FALSE

IF SELF:CreationTime:Hour >= 12

SELF:MorningFile := FALSE

IF SELF:CreationTime:Hour >= 22

SELF:LateNightFile := TRUE

ENDIF

ELSE

SELF:MorningFile := TRUE

ENDIF

RETURN

END CLASS

Here you will see we have used a few properties and methods of the DateTime structure to find out some useful things about our file.

We have also used a "DayOfWeek" enumeration which is relatively self explanatory - if in doubt view the SDK.

There are such a range of properties and methods (plus a couple of fields) that we can't expect to use them all in one small example app. You can think of some good way to test the others yourself ;-)

Okay so far, what we now need to do is have a form applet which will provide a list of file names in a selected folder (using the BrowseFolder dialog as in section 15) see below :-

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

From the names in this list we can make a FileInfo object of each in turn and from the DateTime property values provided by the class we can then instantiate a FileTimes object for each one in the list.

As we make the FileTimes objects we can store them in an arraylist for use later. Lets see what this code may look like, after first seeing that it works OK by viewing the screen output. You will notice that there is now an extra push button called "Create File Times Array" and this is simply used to set this next stage of the process into action :-

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

The code shown below does what we explained above that we were after. Check it through to see that we now have a long list of FileType objects (actually 80 in all for the folder chosen) stored into an arraylist object which has been added to dynamically.

METHOD DoCreateFileTimesArray() AS VOID

LOCAL i, nLen AS DWORD

LOCAL cFileName, cShort AS STRING

LOCAL oFI AS FileInfo

LOCAL oFT AS FileTimes

nLen := SELF:aFiles:Length

FOR i := 1 UPTO nLen

cFileName := aFiles[i]

cShort := aShortFiles[i]

oFI := FileInfo{cFileName}

oFT := FileTimes{cShort, oFI:CreationTime, oFI:LastWriteTime}

SELF:aListFileTimes:Add(oFT)

NEXT

MessageBox.Show("Number of items : "+SELF:aListFileTimes:Count:ToString(),"Make ArrayList :")

RETURN

For our next task, let us try and select a sub-set of these FileTimes objects, and put them into a second arraylist.

How about this year's files - only those made in 2008 ? We will attempt to eventually have their names shown in a list box at the left of the form.

See below for the code to find this year's files

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

If I now fill the list box with these names - the 18 shown for my folder - we can then adapt the code to use time to select late night files, and morning files.

Lets fill the list box with the an array made from just the file names in the objects in the arraylist :- here is the code ...

nCount := SELF:aListThisYear:Count

//MessageBox.Show("Number found :- "+nCount:ToString(),"This Year's files :")

aNamesThisYear := STRING[]{nCount}

FOR i := 0 UPTO nCount-1

oFT := (FileTimes)SELF:aListThisYear[i]

aNamesThisYear[i+1] := oFT:Name

NEXT

//nLen := aNamesThisYear:Length

//MessageBox.Show("Length is :-"+nLen:ToString(),"Names Array :")

SELF:olbxThisYear:Items:AddRange(aNamesThisYear)

The message boxes were just used in testing, but you may find them useful when trying to get your code to work like this, so I have left then in the full code sub-section.

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

Okay, so we have our list box, and it is filled with this years files. Now lets change the code for a time restriction / condition rather than a date aspect.

Well here they are with code alongside - not many, obviously I must be an early bird for file creation ;-)

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

Please notice I have made a slight error in the code above, if it is to find late night files when run in the morning - like at present ;-)

The code line "IF nHour < oDT:Hour" will of course depend on when you run the application. It came from me changing the reasonably OK line for the year in the previous task, when the code is correct.

For the code in the full working section later, I have amended this to "IF nHour < 18" .... which means we only get a list of files created after 18:00 hours.

I will also check out that I get this similarly correct for the morning files. While on the subject of putting things to rights, it is also a good idea to include the following line of code before we add to the combo box :-

SELF:olbxThisYear:Items:Clear()

as you can see it is to clear the control first, before we add elements, otherwise as we click the push buttons for Last Year, Late Night, and Morning, the list will just keep growing. I know I just did it :-) You do realise I am developing this demo / test app as we go along, and I am using Click Start to remind me of working syntax, just as I am recommending to you. It does work, doing like this !

So to complete this sub-section on time, let us see what our developing app form now looks like, once we have added the morning files option :-

You will notice we have also got a couple of labels with 3D edges, to show which file set is loaded, and how large the set is.

Finally, on this test / demo app, you will need to build in some checks for yourself. To handle the eventuality of folders with zero files, and sets which are empty. At the moment the simple code we have developed tends to assume that lists will have items in them :-)

Good Luck with testing and improving. You may like to add a few more buttons and click events yourself.

PHH March 2008.


General use - TimeSpan

The TimeSpan class in the System namespace of .NET allows us to easily handle differences in time.

Remember to have the Framework 2.0 SDK documentation readily available to check out more of this class than we will be able to cover here.

Obviously, before we can have any time difference, we need to have two times which need to be compared. Here a 'time' is actually going to be a DateTime object, so lets us make two of these to use with our code testing.

Shall we have ten minutes past three on Christmas day 2007, and now as I write this - it is eight minutes past nine on November 25th 2007. Let us make these two DateTime 'entites' below :-

Now that we have these two defined times as DateTime 'entities' we may process them to get a time difference.

Let us do this first, before we see what other useful things we may be able to do with time differences.

Although the output is not formatted very well, it does tell me that there are thirty days between just a few minutes ago and our set time on Christmas day afternoon. The other figures represent 6 hours and 2 minutes.

You may be interested to know that if we took the difference as "dayX - dayN" then we would not get an error but instead a negative difference of the same magnitude as above.

Quite commonly we wish to find a time difference as a measure of how long we have been doing a task in our program execution. For example, how long it takes to read in a long data file from disk, or how long an external process like data backup has taken.

Let us set up a code situation where we measure such a time difference. Although it is a little artificial, we can write some code so that we waste time between when we first take a time measurement and when we take the subsequent measurement. Lets see how this goes.

We now need to do some work on presenting the time difference in some better formats so that it is easier to read and probably more useful to us too.

Using the intellisense facility we can see that it is possible to find the time difference in different units from days to milliseconds. Each of these values is returned as data type DOUBLE.

Let us complete the typing of the code and see what we get for the number of milliseconds in our test situation.

If you wish to be scientific about this you can now calculate just how long it has taken to do the processing by subtracting the 1313 milliseconds we asked for a pause.

About 5.3 milliseconds if that is of interest to you !

Before we move on to another topic, notice the way we write our code to get it to pause for a set number of milliseconds.

If you include the code "USING System.Threading" at the head of your PRG file then this may be shortened to "Thread.Sleep(1313)".

At this point you ought to consult the Framework 2.0 SDK documentation and also try out a number of other things which may be done with time differences. Or should we say the TimeSpan class.

As an exercise try adding two time span objects together, see if you may have a negative time span too.


Full working code - DateTime/TimeSpan

LOCAL cText AS STRING

LOCAL oDT AS DateTime

LOCAL dWhen, dToday AS DateTime

oDT := DateTime{2008,03,13}

dToday := DateTime.Now

dWhen := dToday:AddDays(110)

cText := "New date is :-"+dWhen:ToShortDateString()+CRLF

MessageBox.Show(cText,"DateTime class : Examples : ")

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

LOCAL cText AS STRING

LOCAL oDT AS DateTime

LOCAL dWhen, dToday AS DateTime

oDT := DateTime{2008,03,13}

dWhen := oDT:AddDays(-99)

cText := "New date is :-"+dWhen:ToShortDateString()+CRLF

MessageBox.Show(cText,"DateTime class : Examples : ")

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

LOCAL cText AS STRING

LOCAL oDT AS DateTime

oDT := DateTime{2008,03,13}

cText := "Day is :- "+oDT:Day:ToString()+CRLF

cText += "Month :- "+oDT:Month:ToString()+CRLF

cText += "Year :-"+oDT:Year:ToString()+CRLF

cText += "Week day :- "+oDT:DayOfWeek:ToString()+CRLF

cText += "Day of Year :- "+oDT:DayOfYear:ToString()+CRLF

MessageBox.Show(cText,"DateTime class : Examples : ")

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

LOCAL cText AS STRING

LOCAL oDT AS DateTime

oDT := DateTime{2008,03,13}

cText := "Long Date is :- "+oDT:ToLongDateString()+CRLF

cText += "Short Date is :- "+oDT:ToShortDateString()+CRLF

cText += "What is this ??? "+oDT:ToOADate():ToString()+CRLF

MessageBox.Show(cText,"DateTime class : Examples : ")

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

/*

LOCAL dayN, dayX AS DateTime

LOCAL cText, cHead AS STRING

dayX := DateTime{2007,12,25,15,10,00}

dayN := DateTime{2007,11,25,09,08,00}

cText := "Long form :- "+dayX:ToLongTimeString()+CRLF

cText += "Short form :- "+dayN:ToShortTimeString()+CRLF

cHead := "TimeSpan : Examples :"

//MessageBox.Show(cText, cHead)

LOCAL OurDiff AS TimeSpan

OurDiff := dayX - dayN

cText := "Our difference :- "+OurDiff:ToString()

cHead := "TimeSpan : Examples :"

//MessageBox.Show(cText, cHead)

LOCAL OSD AS TimeSpan

LOCAL TimeBefore, TimeAfter AS DateTime

TimeBefore := DateTime.Now

System.Threading.Thread.Sleep(1313)

TimeAfter := DateTime.Now

OSD := TimeAfter - TimeBefore //---- Our Short Difference ----

cText := "Total milliseconds :- "+OSD:TotalMilliseconds:ToString()+CRLF

cHead := "TimeSpan : Examples :"

MessageBox.Show(cText, cHead)

*/

/*LOCAL oDT1, oDT2, oDT3 AS DateTime

LOCAL nR1, nR2, nR3, nSum AS INT

oDT1 := DateTime.Parse("Mar 13, 2008")

oDT2 := DateTime.Parse("13/3/08")

oDT3 := DateTime.Parse("13/03/2008")

nR1 := Math.Abs(oDT1:CompareTo(oDT2))

nR2 := Math.Abs(oDT1:CompareTo(oDT3))

nR3 := Math.Abs(oDT2:CompareTo(oDT3))

nSum := nR1+nR2+nR3

MessageBox.Show("Summ of date compates :- "+nSum:ToString(),"Compare date parsing :")

oDT1 := DateTime.Parse("Mar 13, 2008 12:12:12")

oDT2 := DateTime.Parse("13/3/08 12:12:12")

oDT3 := DateTime.Parse("13/03/2008 12:12:12")

nR1 := Math.Abs(oDT1:CompareTo(oDT2))

nR2 := Math.Abs(oDT1:CompareTo(oDT3))

nR3 := Math.Abs(oDT2:CompareTo(oDT3))

nSum := nR1+nR2+nR3

MessageBox.Show("Sum of date and time compares :- "+nSum:ToString(),"Compare time parsing :") */

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

[ see very bottom for Function Start() ]

CLASS FileTimes

EXPORT Name AS STRING

EXPORT CreationTime AS DateTime

EXPORT LastWriteTime AS DateTime

EXPORT FridayFile AS Boolean

EXPORT LastYearFile AS Boolean

EXPORT ThisYearFile AS Boolean

EXPORT DecemberFile AS Boolean

EXPORT MorningFile AS Boolean

EXPORT LateNightFile AS Boolean

EXPORT HowOldAsOfNow AS TimeSpan

EXPORT FromCreationToUpdate AS TimeSpan

EXPORT SinceLastUpdate AS TimeSpan

PROTECT Now AS DateTime

PROTECT ThisYear AS DWORD

PROTECT ThisMonth AS DWORD

CONSTRUCTOR(cName AS STRING, dtCreation AS DateTime, dtModify AS DateTime)

SUPER()

SELF:Name := cName:Trim()

SELF:CreationTime := dtCreation

SELF:LastWriteTime := dtModify

SELF:Initialize()

RETURN

METHOD Initialize() AS VOID

SELF:Now := DateTime.Now

SELF:ThisMonth := SELF:Now:Month

SELF:ThisYear := SELF:Now:Year

LOCAL oDTW AS DayOfWeek

oDTW := DayOfWeek.Friday

IF SELF:CreationTime:DayOfWeek == oDTW

SELF:FridayFile := TRUE

ENDIF

IF SELF:CreationTime:Month == 12

SELF:DecemberFile := TRUE

ELSE

SELF:DecemberFile := FALSE

ENDIF

IF SELF:CreationTime:Year == SELF:ThisYear

SELF:ThisYearFile := TRUE

ELSE

SELF:ThisYearFile := FALSE

ENDIF

IF SELF:CreationTime:Year == SELF:ThisYear-1

SELF:LastYearFile := TRUE

ELSE

SELF:LastYearFile := FALSE

ENDIF

SELF:HowOldAsOfNow := SELF:Now - SELF:CreationTime

SELF:FromCreationToUpdate := SELF:LastWriteTime - SELF:CreationTime

SELF:SinceLastUpdate := SELF:Now - SELF:LastWriteTime

SELF:LateNightFile := FALSE

IF SELF:CreationTime:Hour >= 12

SELF:MorningFile := FALSE

IF SELF:CreationTime:Hour >= 22

SELF:LateNightFile := TRUE

ENDIF

ELSE

SELF:MorningFile := TRUE

ENDIF

RETURN

END CLASS

CLASS DisplayFileTimes INHERIT System.Windows.Forms.Form

PROTECT olbFocusNum AS System.Windows.Forms.Label

PROTECT olbInFocusSet AS System.Windows.Forms.Label

PROTECT opbFindMorningFiles AS System.Windows.Forms.Button

PROTECT opbFindLateNightFiles AS System.Windows.Forms.Button

PROTECT olbxThisYear AS System.Windows.Forms.ListBox

PROTECT opbFindThisYearsFiles AS System.Windows.Forms.Button

PROTECT opbCreateFileTimesArray AS System.Windows.Forms.Button

PROTECT ocbAllFileNames AS System.Windows.Forms.ComboBox

PROTECT opbSelectFolder AS System.Windows.Forms.Button

PROTECT opbClose AS System.Windows.Forms.Button

// User code starts here (DO NOT remove this line) ##USER##

EXPORT DataFolder, FilePath AS STRING

EXPORT aFiles, aShortFiles AS STRING[]

EXPORT aListFileTimes AS ArrayList

EXPORT aListThisYear AS ArrayList

EXPORT aListLateNight AS ArrayList

EXPORT aListMorning AS ArrayList

EXPORT aThisYear AS OBJECT[]

CONSTRUCTOR()

SUPER()

SELF:InitializeForm()

SELF:Initialize()

RETURN

METHOD Initialize() AS VOID

SELF:aListFileTimes := ArrayList{}

RETURN

METHOD InitializeForm() AS VOID

// IDE generated code (please DO NOT modify)

SELF:Name := "DisplayFileTimes"

SELF:SuspendLayout()

SELF:Location := System.Drawing.Point{100,100}

SELF:ClientSize := System.Drawing.Size{570,374}

SELF:Text := "Display File Times :"

SELF:olbFocusNum := System.Windows.Forms.Label{}

SELF:olbFocusNum:Name := "lbFocusNum"

SELF:olbFocusNum:Location := System.Drawing.Point{ 176 , 40 }

SELF:olbFocusNum:Size := System.Drawing.Size{ 104 , 23 }

SELF:olbFocusNum:BorderStyle := System.Windows.Forms.BorderStyle.Fixed3D

SELF:olbFocusNum:Text := "total num ..."

SELF:olbFocusNum:ForeColor := System.Drawing.Color.FromArgb( 0,0,255 )

SELF:olbFocusNum:Font := System.Drawing.Font{ "Microsoft Sans Serif" , 9.75 , System.Drawing.FontStyle.Regular }

SELF:olbFocusNum:TabIndex := 9

SELF:Controls:Add(SELF:olbFocusNum)

SELF:olbInFocusSet := System.Windows.Forms.Label{}

SELF:olbInFocusSet:Name := "lbInFocusSet"

SELF:olbInFocusSet:Location := System.Drawing.Point{ 176 , 8 }

SELF:olbInFocusSet:Size := System.Drawing.Size{ 224 , 23 }

SELF:olbInFocusSet:BorderStyle := System.Windows.Forms.BorderStyle.Fixed3D

SELF:olbInFocusSet:Text := "will show in focus set ..."

SELF:olbInFocusSet:ForeColor := System.Drawing.Color.FromArgb( 255,0,0 )

SELF:olbInFocusSet:Font := System.Drawing.Font{ "Microsoft Sans Serif" , 9.75 , System.Drawing.FontStyle.Regular }

SELF:olbInFocusSet:TabIndex := 8

SELF:Controls:Add(SELF:olbInFocusSet)

SELF:opbFindMorningFiles := System.Windows.Forms.Button{}

SELF:opbFindMorningFiles:Name := "pbFindMorningFiles"

SELF:opbFindMorningFiles:Location := System.Drawing.Point{ 176 , 280 }

SELF:opbFindMorningFiles:Size := System.Drawing.Size{ 128 , 23 }

SELF:opbFindMorningFiles:Text := "Find Morning Files"

SELF:opbFindMorningFiles:Click += System.EventHandler{ SELF , @pbFindMorningFilesClick() }

SELF:opbFindMorningFiles:TabIndex := 7

SELF:Controls:Add(SELF:opbFindMorningFiles)

SELF:opbFindLateNightFiles := System.Windows.Forms.Button{}

SELF:opbFindLateNightFiles:Name := "pbFindLateNightFiles"

SELF:opbFindLateNightFiles:Location := System.Drawing.Point{ 176 , 312 }

SELF:opbFindLateNightFiles:Size := System.Drawing.Size{ 128 , 23 }

SELF:opbFindLateNightFiles:Text := "Find Late Night Files"

SELF:opbFindLateNightFiles:Click += System.EventHandler{ SELF , @pbFindLateNightFilesClick() }

SELF:opbFindLateNightFiles:TabIndex := 6

SELF:Controls:Add(SELF:opbFindLateNightFiles)

SELF:olbxThisYear := System.Windows.Forms.ListBox{}

SELF:olbxThisYear:Name := "lbxThisYear"

SELF:olbxThisYear:Location := System.Drawing.Point{ 0 , 8 }

SELF:olbxThisYear:Size := System.Drawing.Size{ 168 , 360 }

SELF:olbxThisYear:TabIndex := 4

SELF:Controls:Add(SELF:olbxThisYear)

SELF:opbFindThisYearsFiles := System.Windows.Forms.Button{}

SELF:opbFindThisYearsFiles:Name := "pbFindThisYearsFiles"

SELF:opbFindThisYearsFiles:Location := System.Drawing.Point{ 176 , 344 }

SELF:opbFindThisYearsFiles:Size := System.Drawing.Size{ 128 , 23 }

SELF:opbFindThisYearsFiles:Text := "Find This Year's Files"

SELF:opbFindThisYearsFiles:Click += System.EventHandler{ SELF , @pbFindThisYearsFilesClick() }

SELF:opbFindThisYearsFiles:TabIndex := 5

SELF:Controls:Add(SELF:opbFindThisYearsFiles)

SELF:opbCreateFileTimesArray := System.Windows.Forms.Button{}

SELF:opbCreateFileTimesArray:Name := "pbCreateFileTimesArray"

SELF:opbCreateFileTimesArray:Location := System.Drawing.Point{ 400 , 72 }

SELF:opbCreateFileTimesArray:Size := System.Drawing.Size{ 160 , 23 }

SELF:opbCreateFileTimesArray:Text := "Create File Times Array"

SELF:opbCreateFileTimesArray:Click += System.EventHandler{ SELF , @pbCreateFileTimesArrayClick() }

SELF:opbCreateFileTimesArray:TabIndex := 3

SELF:Controls:Add(SELF:opbCreateFileTimesArray)

SELF:ocbAllFileNames := System.Windows.Forms.ComboBox{}

SELF:ocbAllFileNames:Name := "cbAllFileNames"

SELF:ocbAllFileNames:Location := System.Drawing.Point{ 400 , 40 }

SELF:ocbAllFileNames:Size := System.Drawing.Size{ 161 , 21 }

SELF:ocbAllFileNames:TabIndex := 2

SELF:Controls:Add(SELF:ocbAllFileNames)

SELF:opbSelectFolder := System.Windows.Forms.Button{}

SELF:opbSelectFolder:Name := "pbSelectFolder"

SELF:opbSelectFolder:Location := System.Drawing.Point{ 472 , 8 }

SELF:opbSelectFolder:Size := System.Drawing.Size{ 91 , 23 }

SELF:opbSelectFolder:Text := "Select Folder"

SELF:opbSelectFolder:Click += System.EventHandler{ SELF , @pbSelectFolderClick() }

SELF:opbSelectFolder:TabIndex := 1

SELF:Controls:Add(SELF:opbSelectFolder)

SELF:opbClose := System.Windows.Forms.Button{}

SELF:opbClose:Name := "pbClose"

SELF:opbClose:Location := System.Drawing.Point{ 488 , 344 }

SELF:opbClose:Size := System.Drawing.Size{ 75 , 23 }

SELF:opbClose:Text := "Close"

SELF:opbClose:Click += System.EventHandler{ SELF , @pbCloseClick() }

SELF:opbClose:TabIndex := 0

SELF:Controls:Add(SELF:opbClose)

SELF:ResumeLayout()

RETURN

METHOD pbFindMorningFilesClick(o AS OBJECT , e AS System.EventArgs) AS VOID

SELF:DoFindMorningFiles()

RETURN

METHOD pbFindLateNightFilesClick(o AS OBJECT , e AS System.EventArgs) AS VOID

SELF:DoFindLateNightFiles()

RETURN

METHOD pbFindThisYearsFilesClick(o AS OBJECT , e AS System.EventArgs) AS VOID

SELF:DoFindThisYearsFiles()

RETURN

METHOD DoFindThisYearsFiles() AS VOID

LOCAL i, nLen, nCount, nYear AS DWORD

LOCAL oFT AS FileTimes

LOCAL oDT AS DateTime

LOCAL aNamesThisYear AS STRING[]

LOCAL aNamesTY AS OBJECT[]

oDT := DateTime.Now

SELF:aListThisYear := ArrayList{}

nLen := SELF:aListFileTimes:Count

FOR i := 0 UPTO nLen-1

oFT := (FileTimes)SELF:aListFileTimes[i]

nYear := oFT:CreationTime:Year

IF nYear < oDT:Year

LOOP

ENDIF

SELF:aListThisYear:Add(oFT)

NEXT

nCount := SELF:aListThisYear:Count

//MessageBox.Show("Number found :- "+nCount:ToString(),"This Year's files :")

aNamesThisYear := STRING[]{nCount}

FOR i := 0 UPTO nCount-1

oFT := (FileTimes)SELF:aListThisYear[i]

aNamesThisYear[i+1] := oFT:Name

NEXT

//nLen := aNamesThisYear:Length

//MessageBox.Show("Length is :-"+nLen:ToString(),"Names Array :")

SELF:olbxThisYear:Items:Clear()

SELF:olbxThisYear:Items:AddRange(aNamesThisYear)

SELF:olbInFocusSet:Text := " This Year Files :"

SELF:olbFocusNum:Text := " Total : "+nCount:ToString()

RETURN

METHOD DoFindLateNightFiles() AS VOID

LOCAL i, nLen, nCount, nHour AS DWORD

LOCAL oFT AS FileTimes

LOCAL aNamesLateNight AS STRING[]

SELF:aListLateNight := ArrayList{}

nLen := SELF:aListFileTimes:Count

FOR i := 0 UPTO nLen-1

oFT := (FileTimes)SELF:aListFileTimes[i]

nHour := oFT:CreationTime:Hour

IF nHour < 18

LOOP

ENDIF

SELF:aListLateNight:Add(oFT)

NEXT

nCount := SELF:aListLateNight:Count

//MessageBox.Show("Number found :- "+nCount:ToString(),"Late night files :")

aNamesLateNight := STRING[]{nCount}

FOR i := 0 UPTO nCount-1

oFT := (FileTimes)SELF:aListLateNight[i]

aNamesLateNight[i+1] := oFT:Name

NEXT

//nLen := aNamesLateNight:Length

//MessageBox.Show("Length is :-"+nLen:ToString(),"Names Array :")

SELF:olbxThisYear:Items:Clear()

SELF:olbxThisYear:Items:AddRange(aNamesLateNight)

SELF:olbInFocusSet:Text := " Late Night Files :"

SELF:olbFocusNum:Text := " Total : "+nCount:ToString()

RETURN

METHOD DoFindMorningFiles() AS VOID

LOCAL i, nLen, nCount, nHour AS DWORD

LOCAL oFT AS FileTimes

LOCAL aNamesMorning AS STRING[]

SELF:aListMorning := ArrayList{}

nLen := SELF:aListFileTimes:Count

FOR i := 0 UPTO nLen-1

oFT := (FileTimes)SELF:aListFileTimes[i]

nHour := oFT:CreationTime:Hour

IF nHour >= 12

LOOP

ENDIF

SELF:aListMorning:Add(oFT)

NEXT

nCount := SELF:aListMorning:Count

//MessageBox.Show("Number found :- "+nCount:ToString(),"Late night files :")

aNamesMorning := STRING[]{nCount}

FOR i := 0 UPTO nCount-1

oFT := (FileTimes)SELF:aListMorning[i]

aNamesMorning[i+1] := oFT:Name

NEXT

//nLen := aNamesLateNight:Length

//MessageBox.Show("Length is :-"+nLen:ToString(),"Names Array :")

SELF:olbxThisYear:Items:Clear()

SELF:olbxThisYear:Items:AddRange(aNamesMorning)

SELF:olbInFocusSet:Text := " Morning Files :"

SELF:olbFocusNum:Text := " Total : "+nCount:ToString()

RETURN

METHOD pbCreateFileTimesArrayClick(o AS OBJECT , e AS System.EventArgs) AS VOID

SELF:DoCreateFileTimesArray()

RETURN

METHOD DoCreateFileTimesArray() AS VOID

LOCAL i, nLen AS DWORD

LOCAL cFileName, cShort AS STRING

LOCAL oFI AS FileInfo

LOCAL oFT AS FileTimes

nLen := SELF:aFiles:Length

FOR i := 1 UPTO nLen

cFileName := aFiles[i]

cShort := aShortFiles[i]

oFI := FileInfo{cFileName}

oFT := FileTimes{cShort, oFI:CreationTime, oFI:LastWriteTime}

SELF:aListFileTimes:Add(oFT)

NEXT

MessageBox.Show("Number of items : "+SELF:aListFileTimes:Count:ToString(),"Make ArrayList :")

RETURN

METHOD pbSelectFolderClick(o AS OBJECT , e AS System.EventArgs) AS VOID

SELF:DoSelectFolder()

RETURN

METHOD DoSelectFolder() AS VOID

LOCAL oFBD AS FolderBrowserDialog

oFBD := FolderBrowserDialog{}

oFBD:RootFolder := environment.specialfolder.mycomputer

oFBD:ShowDialog()

SELF:FilePath := oFBD:SelectedPath

SELF:DataFolder := oFBD:SelectedPath+"\"

oFBD:Dispose()

SELF:GetFiles()

RETURN

METHOD GetFiles() AS VOID

//LOCAL aFiles, aShortFiles AS STRING[]

LOCAL cSearch AS STRING

LOCAL nLen AS DWORD

cSearch := "*.*"

aFiles := Directory.GetFiles(SELF:DataFolder, cSearch)

aShortFiles := SELF:MakeFilesShort(aFiles)

ocbAllFileNames:Items:AddRange(aShortFiles)

nLen := aShortFiles:Length

MessageBox.Show("Number of files :- "+nLen:ToString())

RETURN

METHOD MakeFilesShort(aFiles AS STRING[]) AS STRING[]

LOCAL i, nLen AS DWORD

LOCAL aShort AS STRING[]

LOCAL cFile, cShort AS STRING

nLen := aFiles:Length

aShort := STRING[]{nLen}

FOR i := 1 UPTO nLen

cFile := aFiles[i]

cShort := SELF:GetOnlyFile(cFile)

aShort[i] := cShort

NEXT

RETURN aShort

METHOD GetOnlyFile(cPath AS STRING) AS STRING

LOCAL cFile AS STRING

LOCAL nPosn AS INT

IF !cPath:Contains("\")

cFile := cPath

ELSE

nPosn := cPath:LastIndexOf("\")

cFile := cPath:Substring(nPosn+1)

ENDIF

RETURN cFile

METHOD pbCloseClick(o AS OBJECT , e AS System.EventArgs) AS VOID

SELF:Close()

RETURN

END CLASS

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

#USING System.Windows.Forms

#USING System.Drawing

#USING System.Reflection

#USING System.Xml

#USING System.IO

#USING System.Xml

#USING System.Xml.Schema

#USING System

#using System.Collections

[STAThreadAttribute];

FUNCTION Start( asCmdLine AS STRING[] ) AS INT

LOCAL nExitCode AS INT

LOCAL oForm AS WarpForm

Application.EnableVisualStyles()

Application.DoEvents()

LOCAL oForm2 AS DisplayFileTimes

oForm2 := DisplayFileTimes{}

Application.Run(oForm2)

RETURN nExitCode

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

PHH March 2008.


Section 19 - Enumeration

One small but far from insignificant feature of .NET is the availability of Enumeration.

Everyone can greatly improve the readability of their code by using this when appropriate.

Let us take a simple example of how it has improved my life as a developer and coder.

Very recently I wished to be able to switch between six or seven different sorting methods from a code line as follows :-

  aList:Sort( PatientRecordAllSorts{x} )

Basically, when "x" has a value of one I wish the first method in the "AllSorts" class to be used, when it has a value of 3, the third, and so on.

However, by using enumeration we get a few benefits. Firstly, it is much more difficult to use the wrong sort if the value of x is provided using "PatientRecordSortType.PostCodeAscend" for example. It is easy to miss that a value of 7 supplied should actually be 8 to force the correct sort method.

Look at the enumeration below :-

ENUM PatientRecordSortType

MEMBER SurnameFirstnamesAZ := 1

MEMBER SurnameFirstnamesZA := 2

MEMBER MostRecentCustomer := 3

MEMBER PatientUniqueReference := 4

MEMBER SurnameAZ := 5

MEMBER SurnameZA := 6

MEMBER LongestWithCompany := 7

MEMBER PostCodeAscend := 8

END ENUM

Please note that Karl Faller and Chris Pyrgas have pointed out that you don't need to use the "MEMBER" syntax, and can in fact miss it out from your code, so that it looks like below :-

ENUM PatientRecordSortType

SurnameFirstnamesAZ := 1

SurnameFirstnamesZA := 2

MostRecentCustomer := 3

PatientUniqueReference := 4

SurnameAZ := 5

SurnameZA := 6

LongestWithCompany := 7

PostCodeAscend := 8

END ENUM

So now when I wish to use a particular sort method I do as follows :-

  LOCAL nMyType as DWORD

  nMyType := PatientRecordSortType.PostCodeAscend

  aList:Sort( PatientRecordAllSorts{nMyType} )

This is fine and also a little trick of putting related comments in the later code also helps coding :-

VIRTUAL METHOD Compare(o1 AS OBJECT, o2 AS OBJECT) AS INT

LOCAL oPatRec1, oPatRec2 AS PatientRecord

oPatRec1 := (PatientRecord)o1

oPatRec2 := (PatientRecord)o2

// Would normally compare first with second for ascending sort

// SurnameFirstnamesAZ >> 1

// SurnameFirstnamesZA >> 2

// MostRecentCustomer >> 3

// PatientUniqueReference >> 4

// SurnameAZ >> 5

// SurnameZA >> 6

// LongestWithCompany >> 7

// PostCode >> 8

LOCAL result AS INT

DO CASE

CASE SELF:WhichSort = 1

result := SELF:SurFirstAZ(oPatRec1, oPatRec2)

CASE SELF:WhichSort = 2

{ you will notice that I missed a trick - pointed out by Paul Piko - I should have used the enumeration in the CASE statement also ! }

{ CASE SELF:WhichSort = PatientRecordSortType.SurnameFirstnamesAZ  }

However, on top of these benefits there is another great one ! Because of using VIDE to code I have intellisense working, and this means I get the correct options available at code input time too. Very nice ! Be careful on the one hand, as the list is in alphabetical order in the intellisense list, not in an order related to the values you may have attached to the text.

Consult Section 2, on ArrayList : All Sorts, to see this in action.

I wish to thank Paul Piko for the virtual method code for sorting ArrayLists. Check out his MindMeld site referred to in the other articles.

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

Just thought you would like to see an interesting little image of
intellisense at work in VIDE.

I have just posted an article in Wiki at GoVulcan, and this image goes hand
in hand with that article.

Don't be fooled by the strange place I was typing in order to get the list
to display - I only put the text there to get the display next to the ENUM
code section.

I suggest you look at using Enumeration whenever you can to help the
readability of your code - if you haven't already discovered it.

Please take notice of my warning in the Wiki article about the A-Z presented
list order NOT matching your own "valued" order. Trivial in one sense, but
it could catch you out :-)

Hi Karl, (Faller)

Thanks for this contribution :-)

I was just following the notes I got from DevFest last year see below :-

ENUM PayBy
MEMBER Check := 1
MEMBER Cash := 2
MEMBER DontPay := 3
END ENUM

Usually, I just copy and paste bits of Chris's code and they work fine - so
this is what I do !

Do you know why "MEMBER" is not necessary ?

Will it always work without it, in all cases ?? No comeback ???

If so, I will make a change (footnote) to the Wiki article saying about your
helpful contribution and suggestion.


Section 20 - Images - BMP, JPG etc.

Enter topic text here.


Pictures on Screen

Hi Guys and Dolls,

I thought I had better try and raise the "tone" a notch after the "Karl & Don show", crying over their curly braces :-)

Just to let you know I discovered the "PictureBox" class while I was doing some demo "stuff" for the VOUG meeting.

I had previously put images on some of my VO screens to brighten things up. Especially my Primary School Library software - see attached image of the review screen.

However, I was always disappointed how limited in VO I was to anything like real control over using and displaying images from file. Including putting signatures on my printed letters. I could only do this by getting a class from Michael Rubinstein (thanks!).

Now things are different, and it is much easier for the standard guy to do a lot more with images in .NET so you will have to give it a go !

Within a short time I was not just displaying a file image, and over the full PictureBox area (easily!), but I was also clicking with the mouse and changing the image too. Really refreshing stuff, if not that useful to some of our community !

I think the images attached show enough of the code for you to try it for yourself.

First you set the event handler (for the picture box object called "SlideShow") as below :-

//-------------- picture box - Image change ----------------
SELF:opbSlideShow:Click += EventHandler{ SELF , @pbSlideShowClick() }

Then the event handler method - which I like to keep free of the actual "action" code, and point to a standard "action" method :-

METHOD pbSlideShowClick(o AS OBJECT , e AS System.EventArgs) AS VOID
SELF:ShowPictureImage()
RETURN

Then we need the "action" method itself :-

METHOD ShowPictureImage() AS VOID

SELF:opbSlideShow:SizeMode := PictureBoxSizeMode.StretchImage

DO CASE
CASE SELF:PicturePointer = 1
SELF:opbSlideShow:Image :=
System.Drawing.Image.FromFile("c:\voug_cd\vdnuk\qc.bmp")
CASE SELF:PicturePointer = 2
SELF:opbSlideShow:Image :=
System.Drawing.Image.FromFile("c:\voug_cd\vdnuk\greatbearlogo.jpg")
CASE SELF:PicturePointer = 3
SELF:opbSlideShow:Image :=
System.Drawing.Image.FromFile("c:\voug_cd\vdnuk\PelLogo1b.bmp")
CASE SELF:PicturePointer = 4
SELF:opbSlideShow:Image :=
System.Drawing.Image.FromFile("c:\voug_cd\vdnuk\imag0488.jpg")
OTHERWISE

ENDCASE

SELF:PicturePointer +=1
IF SELF:PicturePointer > 4
SELF:PicturePointer := 1
ENDIF

RETURN

All I am doing here is keeping a pointer which tracks which of the four images is being displayed and when necessary causes a "cycle", or looping to occur.

If you really want to be "swish", have a look at the Audio article in Wiki and get some music to play in the background - different music for different pictures.

That seems like a good idea for the London VOUG meeting - will have to remember to put in the speakers however - and cables :-)

Have a look in Wiki as I will document this a little better, and also show how you may pick up mouse clicks on one image and turn these into icon numbers - so effectively pointing at different parts of the image - possibly to start a new app to launch. Image is also attached.

Hope this helps.

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

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

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

Back to the problem of clicking an image (stretched across a PictureBox control) and finding out exactly where the mouse pointer is within the bounds of the control.

You may be familiar with the attached image from an earlier "Pearls" VOPS article. It shows a PictureBox control with an image (from file on disk) stretched to fit the customized size of the control. The image is of five desktop icons in a horizontal row.

By the nature of my image, there are five equal areas which can be associated with the icon shown within the image itself. What I wanted to do was simply associate a mouse click with one of the five areas, and hence the icon in the clicked area. A bit like clicking icons on the Desktop.

To be able to make such an association, first I needed to find the coordinates of the mouse pointer when the click event happens. Then I can use some simple mathematics to work out which of the areas has been clicked, by finding the dimensions of the PictureBox itself.

I have set my demo so that there is a MessageBox displayed to show the relevant information, when the mouse is clicked.

Let us see how we can first detect the mouse click event, and then handle it appropriately. The PictureBox control is called "opbFiveIcons" in my example, and below is the section from the InitializeForm() method created by the IDE. Note I have left the setting for the event handler in the InitializeForm section, against my normal practice where I like to gather all the event handler settings into one custom method called "SetMyEventHandlers()". This just proves I am human :-)

SELF:opbFiveIcons := PictureBox{}

SELF:opbFiveIcons:Name := "pbFiveIcons"

SELF:opbFiveIcons:Location := Point{ 0 , 160 }

SELF:opbFiveIcons:Size := Size{ 360 , 96 }

SELF:opbFiveIcons:TabIndex := 11

SELF:opbFiveIcons:MouseDown += MouseEventHandler{ SELF , @pbFiveIconsMouseDown() }

SELF:Controls:Add(SELF:opbFiveIcons)

Now we need to have a few lines at the end of the Constructor() method which will place the image from disk file, onto the PictureBox control and stretch it to fit exactly.

//------------------ put icons JPG on the form --------------------

LOCAL oImage AS Image

oImage := System.Drawing.Image.FromFile("c:\vdnuk\fiveicons.jpg")

SELF:opbFiveIcons:Image := oImage

SELF:opbFiveIcons:SizeMode := PictureBoxSizeMode.StretchImage

I realize that there is an easier way to code this as follows :-

SELF:opbFiveIcons:Image := System.Drawing.Image.FromFile("c:\vdnuk\fiveicons.jpg")

But at the time in my code I was also making the image available on the clipboard, so I did it the first way so I could use :-

Clipboard.Clear()

Clipboard.SetImage(oImage)

to simply provide this same image for another running app to pick up !

Anyway, enough of that clipboard stuff, back to our mouse clicking. Below is the event handler, which uses information supplied by the "MouseEventArgs" object. Remember the joys of intellisense and refer to the other "Pearls" image. For this example, when we are typing in our code, all we have to do to get a list of the available properties and methods relating to "e" is to type "e:" and as soon as the colon appears, the list box is displayed as shown in the reference.

You will see in the code below that we have used some of these properties to good effect.

METHOD pbFiveIconsMouseDown(o AS OBJECT , e AS MouseEventArgs) AS VOID

    LOCAL nX, nY, nPictureWidth, nPW, nBin, nBinW AS LONG

    LOCAL oLocation, oPictureSize AS Point

    LOCAL cText, cBin AS STRING

    nPictureWidth := SELF:opbFiveIcons:Size:Width

    nPW := nPictureWidth

    nX := e:X

    nY := e:Y

    oLocation := e:location

    nBinW := Integer(nPW/5)+1

    nBin := Integer(nX/nBinW)+1

    DO CASE

         CASE nBin = 1

              cBin := "Vulcan Printing"

         CASE nBin = 2

              cBin := "Slide Show"

         CASE nBin = 3

              cBin := "BEST Project 1"

         CASE nBin = 4

              cBin := "First DataView"

         CASE nBin = 5

              cBin := "Install VOUGcd"

    ENDCASE

    cText := "Mouse pointer coordinates when clicked in Icons image :-"+CRLF+CRLF

    cText += "X value :-"+NTrim(nX)+CRLF

    cText += "Y value :-"+NTrim(nY)+CRLF

    cText += "Picture bin width :- "+NTrim(nBinW)+CRLF

    cText += "In Picture bin :- "+NTrim(nBin)+CRLF

    cText += "Icon Picture is :- "+cBin+CRLF

    MessageBox.Show(cText)

RETURN

I have named the five areas of the image "bins", so we have bins 1 to 5. I thought it may be a bit confusing to call them "icons" when they weren't really. Also notice that we are only interested in the width of the PictureBox, as the overall width splits into five equal parts. The height of the control is of no interest to us while solving this problem.

Also it is important to note that the information on the X & Y of the click, is given in coordinates related to the PictureBox control, and not the Form, or the screen. This seems obvious when you think about it, as mouse click is a control related event.

What I intended to do, and never got round to it, was to finally link the event to launching the correct application for the icon which is shown in the "bin" area. I will leave you to do that as you can see how to do this from articles in Wiki on launching external apps. And one of your other apps is just another external :-)

Tip to do above - look out the "Process" class in Framework 2.0 SDK for the relevant info, as well as in Wiki and "Pearls" articles.

Hope this provides you with some fun !

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


Section 21 - Using Text Files (streaming)

Enter topic text here.


Stream Writing

Hi Fellow File Creators,

Here are a few tips for those who have not yet found the joy of using the StreamWriter class from dotNet.

I have attached an image of a relevant section of code from a current demo app of mine.

Just before this section shown, I have searched a large body of text data from the registry, to find lines with a target string. Lets say we searched for lines including "Visual Studio".

At the point I wish to create a file I have a StringBuilder object with all the lines of text found - we now wish to place this on disk, in a file. For use later, and possible printing etc.

oSW := StreamWriter{"c:\temp\ResultsDump.txt", FALSE, Encoding.ASCII}
oSW:Write(oSB:ToString())
oSW:Close()
oSW:Dispose()

Notice that this overload for instantiating a StreamWriter object, uses three input parameters.

The first is obviously the file name, and the second says whether the file may be added to (append allowed), the third is one of a few possible "formats".

SO if the FALSE is put to TRUE you add text to the existing file.

You need to consult the Framework 2.0 SDK to find out more than what is available, but, this does work OK for basic text output.

What I find particularly nice about this approach is that you often have the StringBuilder object already built by previous code. For example, in this demo I have filled the results RichTextBox control (on my Form) from such a SB object.

So all I had to do to get a file of my results was a basic four line bit of coding, and two of these are just short lines which are tidying up.

Hope this helps you get going with writing files. Don't forget to add suitable CRLF at the ends of lines however, as this will aid in reading in the data at a later stage :-)


Stream Reading

Reading in data from text files is both easy and fast.

Here is a sample of code from a working customer application I am developing at the moment.

LOCAL cLine, cLeft, cRight AS STRING

LOCAL iLen, iRead AS INT

LOCAL oSR AS StreamReader

oSR := StreamReader{SELF:guVL}

iRead := 0

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

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

DO WHILE .T.

cLine := oSR:Readline()

IF cLine = NULL_OBJECT

EXIT

ENDIF

cLine := cLine:TRIM() //---- format ----[ X| .... text ..... ]-------

iRead +=1

iLen := cLine:Length

cLeft := cLine:Substring(0,1)

cRight := cLine:Substring(2,iLen-2)

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

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

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

ENDDO

oSR:Close()

oSR:Dispose()

oSR := NULL_OBJECT

Messagebox.Show("Lines read were :- "+iRead:ToString())

A line is considered to be the stream of characters ended with a CRLF (pair of characters - CHR(13)+CHR(10) as seen in the ASCII table in Section 11).

The CRLF gets automatically removed in the reading of the line. Quite helpful if that's what you want.

The whole process of reading is usually finished when a NULL line is read.

Do be aware however, that if you try to trim the text line read in, at the same time as reading it, you will get an error.

The trim method for a string object gives an error is the object is a NULL_OBJECT. I know, I tried it, and got an error :-)

PHH December 2007.


Config Files

Enter topic text here.


Section 22 - GUI form controls (II)

Enter topic text here.


Progress Bar

Hi Pearl Progressors !

Just a though about using the ProgressBar class in WinForms.

The attached image shows a progress bar in operation in one of my demo apps. It is imply used to help fill the time it takes to read 11,000 patient records from DBF on disk.

The tool is simple to setup and use, but, there is a small problem which does hit us "standard guys" !

Look at the four lines of code below and you will see that the third line in which I am trying to set the "step" size appears different.

The "@@" is included to force an issue. If the double @ is left out the IDE takes the "step" characters as a command word in Vulcan and forces them to be in red and upper case, as probably doing something in the background. The up-shot of this is that the code is not seen in the way we intend it to be, and an error occurs.

Placing the "@@" before the characters "step" corrects the issue, and you are then OK to get on with your coding.

I suspect this is not the only place in Vulcan coding this may happen when you are using dotNet features, so do remember this trick. I have to confess I have had to ask Chris Pyrgas more than once. Lets hope I remember now that I have put finger to keyboard :-)

SELF:oprgbInput:Minimum := 0

SELF:oprgbInput:Maximum := 11000

SELF:oprgbInput:@@STEP := 200

SELF:oprgbInput:Value := 0

Once the ProgressBar is initialized in the Constructor() as above, it may then be used as in the code below.

I have left in the line (commented out) which I had to use before Chris reminded me of the "@@". If you have not set the step then you have to do the job yourself, with a bit of hard wired code.

Obviously, the benefit of setting the "step" is that this may be done in one place in your code and so this can easily be changed interactively for some operations. For example, it may depend on the number of files present, to be included in a file transfer operation. The value can only be set at run-time when the number is found.

There may be many places in your code where you wish to "progress" your bar, but only one where you wish to set the step. Using the "PerformStep()" method both improves and tightens your code.

nCount +=1

nTCount +=1

IF nTCount >= 200

nTCount := 0

olbCount:TEXT := " <"+NTrim(nCount)+">"

Application.DoEvents()

SELF:oprgbInput:PerformStep()

//SELF:oprgbInput:Value += 200

ENDIF

IF nCount >= 11000

EXIT

ENDIF

oServerPA:Skip(+1)

ENDDO

I hope this helps you use the progress bar in your applications in future.

You may be interested to know that the "course" count is just to find when 11,000 records have been read from DBF file. The "fine" count is simply used to increment the display of the number read, which is displayed in a label, and this is why we need "Application.DoEvents()" to force the refresh of this label display.

Obviously, the "fine" count also triggers the "PerformStep()" method, so our PB moves forward too.


ListBox

First thing we need to realise is that both ListBox and ComboBox controls hold list of objects.

Because this 'ClickStartVDN' help file is designed to get folk going quickly, who are new to Vulcan and .NET, it will be sufficient to show how to get a list of STRING objects into the control, and access these in a simple and useful way.

Once we get going with the basics we will need to look further into the ListBox.ObjectCollection class, and how it can be used to make our life easier.

First we need to just get a ListBox control on a form and fill it with a list of strings. Be able to change the order of the presented list objects, and to set event handlers for the list box.

This should be enough to get you started and to get you to want to learn more :-)

The first loop is using a VO dynamic array called ColHeads which has the first element of each of a set of sub-arrays as the heading for the column.

The string objects are being added one at a time. This is very VO like, and although it works there are more useful ways to learn.

FOR i := 1 UPTO ALen(SELF:Owner:ColHeads)

SELF:olbColumnNames:Items:Add(SELF:Owner:ColHeads[i,1])

NEXT

If we examine the code below we will see a new approach. That is adding a whole array of objects in one go. However, before we can do this we need to NOT have an array which is VO dynamic in nature, holding USUAL vales.

In future we will try to move away from VO arrays as there is much to benefit from the newer dotNet approach and using dotNet arrays.

For now, let's just see the syntax by firstly converting the dynamic array into an array of sting objects.

LOCAL aCorrect AS STRING[]

LOCAL iLen AS INT

iLen := SELF:Owner:ColHeads:Length

aCorrect := STRING[]{iLen}

FOR i := 1 UPTO iLen

aCorrect[i] := SELF:Owner:ColHeads[i,1]

NEXT

SELF:olbColumnNames:Items:AddRange(aCorrect)

The last code line allows us to add the whole of the array into the ListBox. We can either clear the list box control first, for a "new fill", or just add to what is there by not clearing it.

To empty the list, or clear it, use the following :-

olbColumnNames:Items:Clear()

Either way this is pretty neat and we must move away from old habits in order to benefit from the rich pickings in dotNet.

Selecting an item from the list :-

Now that we have a list of items showing we need to be able to select and use one from that list. We need to set an event handler to react to the selection of a list item.

In my example I wished to handle the "click" of a list item, but thereare others you need to check out - 'DoubleClick', 'SelectedValueChanged', and 'SelectedIndexChanged' are just three.

Below is the line of code in my "SetMyEventHandlers()" method.

SELF:olbColumnNames:Click += System.EventHandler{ SELF,@ColumnNamesClick() }

This basically is telling program execution to next do the "ColumnNamesClick" method after a list item is clicked. This handler method is shown below :-

METHOD ColumnNamesClick(o AS OBJECT , e AS System.EventArgs) AS VOID

LOCAL cItem AS STRING

LOCAL nPosn AS LONG

cItem := SELF:olbColumnNames:SelectedItem:ToString()

nPosn := AScan(SELF:NewOrder, {|x| x == cItem})

IF nPosn < 1

SELF:olbColsSelected:Items:Add(cItem)

AADD(SELF:NewOrder, cItem)

ENDIF

RETURN

Most of this method is actually for after the list item has been retrieved. It is then added to the growing list in the "selected" columns list, but only if it is not there already. Some of this code was from early in my Vulcan exploration so it is VOish in style. Still works however and that is the beauty of Vulcan :-)

If you wish to sort a list box where data is not 'bound' you can set the SORT property to TRUE. However, you could do your own thing by setting an event handler to react to a DoubleClick and then use you own method to order the data items into whatever order you please.

When you are more experience you may even use a data source which support its own sorting. Look into using an arraylist which you can sort using the approach shown in Section 2.

Don't forget to look at the end of the next sub-section where you will see how to load a list when you start with an ArrayList. This is simply, and easily, converted to an array and applied to the list box. A couple of lines that's all.

PHH December 2007.


ComboBox

Don't miss the part at the end of this section where we will cover loading Combo (and other list boxes) with dotNet arrays and arraylists - the easy way.

One VERY common, and flexible, control in GUI is the combo box. We all need to use this on a regular basis I suspect. ( Much of what is said below also applies to List boxes. )

I also know that coming to VDN from VO it was one of the first things I tried to do. I also have to admit that I got very frustrated as I took ages (about 40 minutes) to fill my first one with items, after being able to do this in seconds in VO, even without really thinking too much either.

The problem was simple - I was unfamiliar with the syntax, and was in a rush to get results - I also did not have any "ClickStart" to power me through :-)

On top of this the dotNet SDK and ideas were strange to me - AND - I was trying to use VO Array types!

VO arrays and dotNet don't mix very well at all.

I recommend moving to dotNet arrays, and array lists, to make your life easier and get the most from dotNet generally.

Lets take our first simple example. We will open a DBF and read the last twenty patient unique reference numbers from it into a dotNet array and then use this to fill our Combo control.

Yes, you could do it in one go but we are also seeing new syntax for other code too, so lets go for it.

METHOD FillPatients() AS VOID

LOCAL oServerPT AS DBServer

//-------------------------- open Order ------------------------------

oServerPT := DBServer{SELF:guData+"PAT32"}

oServerPT:GoTop()

oServerPT:ClearIndex()

LOCAL iCount, i, nPatRef AS INT

LOCAL cPatRef AS STRING

SELF:aPatients := STRING[]{20}

iCount := 1

oServerPT:GoBottom()

DO WHILE !oServerPT:BOF

IF !oServerPT:fieldget("patactive")

oServerPT:Skip(-1)

LOOP

ENDIF

nPatRef := oServerPT:fieldget("patunref")

cPatRef := nPatRef:ToString()

aPatients[iCount] := cPatRef

iCount +=1

IF iCount > 20

EXIT

ENDIF

oServerPT:Skip(-1)

ENDDO

oServerPT:Close()

oServerPT := NULL_OBJECT

ocbPatients:Items:Clear()

FOR i := 1 UPTO 20

cPatRef := aPatients[i]

ocbPatients:Items:Add(cPatRef)

NEXT

ocbPatients:SelectedIndex := 1

RETURN

Obviously, once we have the sting array, all it takes to fill the drop down box is four lines of code near the end of the method shown.

[ Revisiting this many months later I can say that because the aPatients array is not a VO array we can actually fill the combo box in one easy line using the AddRange() method. See the end of this section for more similar advice.]

Emptying the list before filling :-

If by any chance your task in hand requires the list to be emptied before nay items are added then on line is required. To empty, or clear, the combo box called "ChosenSettings" then use the "Clear" method in the items colection class as follows :-

ocbChosenSettings:Items:Clear()

One nice thing is that once we know the syntax for filling a combo box we can copy the process to fill a list box too. Below is code from a working demo app to fill the available column names list box in the example discussed in RadioButtons Section 1.5.

LOCAL iLen AS INT

iLen := SELF:Owner:ColHeads:Length

FOR i := 1 UPTO iLen

SELF:olbColumnNames:Items:Add(SELF:Owner:ColHeads[i,1])

NEXT

See more about ListBox controls in the previous sub-section.

Now, to make VOers more comfortable we can shorten this approach to add the whole dotNet array in one line, as shown below :-

ocbPatients:Items:AddRange(aPatients)

Here we add the whole array at one go. We need to find out in future how to add a range with only part of the array. This can be your homework :-)

This replaces the FOR loop lines in the first example. Before you get too "gung-ho" about things be warned, mixing VO dynamic arrays and this dotNet approach is fraught with danger :-) My suggestion is give up VO dynamic arrays which have USUAL data type items.

Selecting an item from the Combo Box :-

Here is a simple way which works OK for simple objects stored, however, we need to look at getting at properties of more complex objects at a later stage.

SELF:SelectedPrinter := ocbSelectPrinter:SelectedItem:ToString()

SELF:otbSelectedPrinter:TEXT := SELF:SelectedPrinter

This code returns to the string variable "SelectedPrinter" the sting representation of the object selected. This is fine if the items are simple like strings and numeric in form. For now let's just use it as it is.

In our GUI screen we need to set a handler for an event of the combo box which is related to selecting an item. I like to use the "SelectionChangeCommitted" event as I get it to do as I require, however, there are a couple more which would probably fit the bill for you.

SELF:ocbPageNum:SelectionChangeCommitted += System.EventHandler{ SELF , @cbPageNumSelectionChangeCommitted() }

When this handler has been set in the constructor, or my PosiInit() method I use a lot, the selection of an item in the combo box - in this case a page to be shown in a RichTextBox control - causes the method called "cbPageNumSelectionChangeCommitted" to execute.

Note I could have shortened the name to "cbPageNumber" but I like to see what is going to cause the method to be used. It is easier to follow the code afterwards if you do keep to long and meaningful names, I feel.

Below is the handler method, all it does is use the index of the selected item to set the pointers for the page in focus and cause a display of the said page. The adjusting of the index is because the combo box is zero based and my Vulcan arrays are one based. You will get used to this adjustment situation - after a bit of trial and error it becomes obvious which you generate and which the system generates for itself.

METHOD cbPageNumSelectionChangeCommitted(o AS OBJECT , e AS System.EventArgs) AS VOID

//-------- choose a new page to display ---------

SELF:PageInFocus := ocbPageNum:SelectedIndex+1

SELF:PagePointer := ocbPageNum:SelectedIndex+1

SELF:DisplayPageInFocus()

RETURN

Getting List Box controls to react to GUI events is very similar - see the earlier sub-section.

DotNet approach to filling list controls with arrays :-

The AddRange() method is available to developers in a number of places throughout the dotNet classes. It is very easy and flexible to use, and fits in well with using collections provided from dotNet static methods.

Let us look at making some of our own arrays and list - then slotting then into a combo box. Because this is a special type of list box, it also applies to list boxes too.

Below is a section of working code which fills a combo box with data types used to apply to the columns of a data table before it is filled with CSV data in the File Multi Converter app.

SELF:aTypes := STRING[]{10}

aTypes[1] := "Float"

aTypes[2] := "Real8"

aTypes[3] := "Double"

aTypes[4] := "Int32"

aTypes[5] := "Dword"

aTypes[6] := "Decimal"

aTypes[7] := "Date"

aTypes[8] := "DateTime"

aTypes[9] := "Boolean"

aTypes[10] := "String"

ocbDataTypes:items:AddRange(aTypes)

#using System.Collections //--------- needed for ArrayLists ----------

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

This is an extremely useful way of doing things. The array list may be built up, adding elements one by one, when the code does not know how many elements to expect, then have an array made at the end of the building process, and applied directly to the combo box.

This approach will probably make VOers smile !

PHH December 2007.


Section 22 - GUI form controls (III)

Enter topic text here.


ListView

Hi Pearl Pickers .....

Below are a couple of shots of a test ListView control which has been filled with data from a DBF.

The code for this demo app is included in the attached RTF file (bottom of page).

All the code relevant to the setting up of the control, and the sample double click event handler are included.

Note the dark blue row highlighted when the cell is double clicked, does not show in the screen capture for the "Ben Fellings" selection from the fictitious patient data.

It is better to use the 'Text' property for the item than use the general ToSting() method.

Hope this helps you get going.

Finally, note I set the fore and back colours for the control in the IDE Properties/General tab - I was using VIDE from Chris Pyrgas.

Good Luck,
Phil.

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

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

Using the ListView control - WinForms :

Below is code to setup and display a ListView control - with the data being provided by a DBF file with CDX indexing.

The code inserted by the IDE is also included, as well as a basic event handler method for the double click event.

I hope this helps you get up and running. Don't forget to consult the SDK documentation for Framework 2.0 - which is very informative if studied closely, and bits added and tested so you get a feel for what you are doing.

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

PROTECT olvTests AS ListView

//-------------- section from InitializeForm() --------------

//-------------- Auto placed by IDE ---------------

SELF:olvTests := ListView{}

SELF:olvTests:Name := "lvTests"

SELF:olvTests:Location := Point{ 8 , 232 }

SELF:olvTests:Size := Size{ 336 , 216 }

SELF:olvTests:Alignment := ListViewAlignment.Default

SELF:olvTests:AllowColumnReorder := TRUE

SELF:olvTests:FullRowSelect := TRUE

SELF:olvTests:GridLines := TRUE

SELF:olvTests:Sorting := SortOrder.None

SELF:olvTests:ForeColor := Color.FromArgb( 0,0,170 )

SELF:olvTests:BackColor := Color.Aquamarine

SELF:olvTests:DoubleClick += EventHandler{ SELF ,

@lvTestsDoubleClick() }

SELF:olvTests:View := View.Details

SELF:olvTests:ColumnClick += ColumnClickEventHandler{ SELF , @lvTestsColumnClick() }

SELF:olvTests:TabIndex := 4

SELF:Controls:Add(SELF:olvTests)

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

//-------------- My Code in Constructor ----------------

SELF:olvTests:Columns:Add("Surname", 80)

SELF:olvTests:Columns:Add("Firstnames", 80)

SELF:olvTests:Columns:Add("Add1", 100)

SELF:olvTests:Columns:Add("Add2", 100)

SELF:olvTests:Columns:Add("Add3", 100)

SELF:olvTests:Columns:Add("Add4", -2)

SELF:olvTests:Columns:Add("Add5", -2)

SELF:olvTests:Columns:Add("PCode", 70)

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

//------------- method triggered by click event ----------------

METHOD PopulateListViewFromDBF AS VOID

LOCAL oItem AS ListViewItem

LOCAL cSurname, cFirstnames, cAdd1, cAdd2, cAdd3, cAdd4, cAdd5, cPcode AS STRING

LOCAL nCount AS LONG

nCount := 0

SELF:olvTests:Items:Clear()

DO WHILE ! oServerPA:eof

oItem := ListViewItem{}

cSurname := AllTrim(oServerPA:Fieldget("surname"))

cFirstnames := AllTrim(oServerPA:Fieldget("firstnames"))

cAdd1 := AllTrim(oServerPA:Fieldget("add1"))

cAdd2 := AllTrim(oServerPA:Fieldget("add2"))

cAdd3 := AllTrim(oServerPA:Fieldget("add3"))

cAdd4 := AllTrim(oServerPA:Fieldget("add4"))

cAdd5 := AllTrim(oServerPA:Fieldget("add5"))

cPcode := AllTrim(oServerPA:Fieldget("pcode"))

oItem:Text := cSurname

oItem:SubItems:Add(cFirstnames)

oItem:SubItems:Add(cAdd1)

oItem:SubItems:Add(cAdd2)

oItem:SubItems:Add(cAdd3)

oItem:SubItems:Add(cAdd4)

oItem:SubItems:Add(cAdd5)

oItem:SubItems:Add(cPcode)

SELF:olvTests:Items:Add(oItem)

nCount +=1

IF nCount > 1300

EXIT

ENDIF

oServerPA:Skip(+1)

ENDDO

RETURN

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

METHOD lvTestsDoubleClick(o AS OBJECT , e AS EventArgs) AS VOID

LOCAL cText, cText1, cText2 AS STRING

LOCAL slvic AS ListView.SelectedListViewItemCollection

LOCAL lvi AS ListViewItem

cText1 := ""

cText2 := ""

MessageBox.Show("You have double clicked the List View !")

slvic := olvTests:SelectedItems

cText := slvic:ToString()

lvi := slvic:item[0] //---------- will do first of many or only one ! -------

cText1 := lvi:Text

cText2 := lvi:SubItems[1]:Text

cText := cText2+" "+cText1

MessageBox.Show(cText)

RETURN

End of code to setup and use a ListView control.

See the attached image of this control in action !

PHH Oct.2007


TreeView

The tree view control is possibly not used as much as it ought to be but now that it is easy to set up and use you have no excuse to not use it more regularly.

Yes, it is rather specialised, but, more data is of a tree structure than you may care to realise.

Let us look at a simple example of how to create the part of the "InstallVOUGcd" app GUI which we used in our local meetings.

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

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

The control has a line in the class declaration like that below, and it is entered automatically by the VIDE.

PROTECT otvInstall AS TreeView

The method below is the one I the used to add data to the TreeView control. Notive I chose to have the nodes show in red.

You will also notice that for each node we fill it with files using the "AddFiles2Node" method.

METHOD AddNodes2Tree()AS VOID

LOCAL cNodeName, cFolder AS STRING

LOCAL oTN AS TreeNode

cFolder := "C:\VDNUK\"

cNodeName := "VDNUK"

otvInstall:Nodes:Add(cNodeName)

otvInstall:Nodes[0]:ForeColor := Color.Red

oTN := otvInstall:Nodes[0]

SELF:AddFiles2Node(cFolder, oTN)

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

cFolder := "C:\VDN_test\"

cNodeName := "VDN_test"

otvInstall:Nodes:Add(cNodeName)

otvInstall:Nodes[1]:ForeColor := Color.Red

oTN := otvInstall:Nodes[1]

SELF:AddFiles2Node(cFolder,oTN)

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

cNodeName :="VOUG_CD"

otvInstall:Nodes:Add(cNodeName)

otvInstall:Nodes[2]:ForeColor := Color.Red

cFolder := "C:\VOUG_CD\FilesNpdf\"

cNodeName := "FilesNpdf"

otvInstall:Nodes[2]:Nodes:Add(cNodeName)

otvInstall:Nodes[2]:Nodes[0]:ForeColor := Color.Red

oTN :=otvInstall:Nodes[2]:Nodes[0]

SELF:AddFiles2Node(cFolder,oTN)

cFolder := "C:\VOUG_CD\TextFilesNCoding\"

cNodeName := "TextFilesNCoding"

otvInstall:Nodes[2]:Nodes:Add(cNodeName)

otvInstall:Nodes[2]:Nodes[1]:ForeColor := Color.Red

oTN :=otvInstall:Nodes[2]:Nodes[1]

SELF:AddFiles2Node(cFolder,oTN)

cFolder := "C:\VOUG_CD\VDNUK\"

cNodeName := "VDNUK"

otvInstall:Nodes[2]:Nodes:Add(cNodeName)

otvInstall:Nodes[2]:Nodes[2]:ForeColor := Color.Red

otvInstall:Nodes[2]:Nodes[2]:Nodes:Add("PRGfiles")

otvInstall:Nodes[2]:Nodes[2]:Nodes[0]:ForeColor := Color.Red

oTN :=otvInstall:Nodes[2]:Nodes[2]

SELF:AddFiles2Node(cFolder,oTN)

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

otvInstall:Nodes:Add("VIDE")

otvInstall:Nodes[3]:ForeColor := Color.Red

otvInstall:Nodes[3]:Nodes:Add("Projects")

otvInstall:Nodes[3]:Nodes[0]:Nodes:Add("BasicVulcanPrinting")

RETURN

The method below finds the file names in the folder for the node and adds these as a list of string values.

METHOD AddFiles2Node(cFolder AS STRING, oTN AS TreeNode) AS VOID

LOCAL AllFiles AS STRING[]

LOCAL nLen,i AS LONG

LOCAL cFile AS STRING

AllFiles := Directory.GetFiles(cFolder)

nLen := AllFiles:Length

IF nLen < 1

RETURN

ENDIF

//MessageBox.Show("files found :- "+NTrim(nLen))

FOR i:= 1 UPTO nLen

cFile := AllFiles[i]

cFile := SELF:FindFileName(cFile)

oTN:Nodes:add(cFile)

NEXT // i

RETURN

If you are to do anything more than display information and aloow expansion and collapsing of the nodes etc. you will need to set an event handler to deal with the situation.

Below is a simple example but only goes as far as reporting back on what has been selected. I will leave it up to you to take things further.

SELF:otvInstall:AfterSelect += TreeViewEventHandler{ SELF , @tvInstallAfterSelect() }

Here I have used the "AfterSelect" event to trigger the handler for the "Install" TreeView control. Below is the simple method to retrieve the name of the node which has been selected.

METHOD tvInstallAfterSelect(o AS OBJECT , e AS TreeViewEventArgs) AS VOID

LOCAL cText AS STRING

//------- try these ------

//e:Node

//e:Action

cText := otvInstall:SelectedNode:TEXT

MessageBox.Show("responding to click in Tree View !"+CRLF+cText)

RETURN

Don't forget to use VIDE's intellisense to find out just what other options are available from the "e" parameter.

Good Tree Viewing :-)

PHH December 2007.


Section 23 - .NET classes to get you Going

This is a short section and simply lists out a set of dotNet classes which may be of use to the developer coming to VDN from Visual Objects.

I made this list up in a simple way. I looked at all my demo applications after about six months or more, and pin-pointed the the dotNet classes which were used.

Obviously these are just a tiny fraction of what exists in Framework 2.0 and 3.0 - but - it is surprising just what useful applications may be made with just these few.

The set of classes has been grouped in a meaningful way as shown below :- ( there are five headings )

Disk / File / Folder :-

File

FileInfo System.IO mscorlib.dll

DriveInfo System.IO mscorlib.dll

DriveType (enumeration) System.IO mscorlib.dll

Directory System.IO mscorlib.dll

DirectoryAttribute System.DirectoryServices.Protocols

StreamReader System.IO mscorlib.dll

StreamWriter System.IO mscorlib.dll

GUI / WinForms :-

Form System.Windows.Forms

MessageBox System.Windows.Forms

DialogResult (enumeration)

RichTextBox System.Windows.Forms

{ controls (I) }

ListBox System.Windows.Forms

ComboBox

ProgressBar { controls (II) }

ListView

ListViewItem

TreeView

TreeNode { controls(III) }

DataGridView System.Windows.Forms system.windows.forms.dll

DataTable { controls(IV) }

( the various system dialog classes - see separate Section 24 'Dialog Boxes' )

Data Structures :-

Array System.Array mscorlib.dll

ArrayList System.Collections mscorlib.dll

SortedList System.Collections mscorlib.dll

Stack System.Collections mscorlib.dll

Queue System.Collections mscorlib.dll

General :-

StringBuilder System.Text mscorlib.dll

Conversion Microsoft.VisualBasic microsoft.visualbasic.dll

CultureInfo System.Globalization mscorlib.dll

DateTime (structure) System mscorlib.dll

TimeSpan (structure) System mscorlib.dll

Font

Clipboard System.Windows.Forms system.windows.forms.dll

Image System.Drawing system.drawing.dll

Audio Microsoft.VisualBasic.Devices microsoft.visualbasic.dll

Process System.Diagnostics system.dll

RegistryKey Microsoft.Win32 mscorlib.dll

Structure / Enumeration :-

Color

Cursor

TextDataFormat

RegistryKey

String.Format (method)


Section 24 - Dialog Boxes

Enter topic text here.


Color

Selecting Color - ColorDialog Class :

The system supplied "Dialog Box" tools can be very useful to the developer, and are quick and easy to use. See also those for Font and Files. There will be short articles on these too in Wiki, just to help you get going.

See also the class for "FolderBrowserDialog" in the SDK for Framework 2.0

The code below shows a method which when called, will display a color dialog box and allow the user to interact with it.

You will need to have a play with this class, and also look it up in the Framework 2.0 SDK. I could not see a difference in what I got on screen when I "set" the "anycolor" property or "unset" it. But I was in a rush!

I am using Vista 32 and know it does funny things with color in Tool Tips (see Wiki article) - like make them black on white - so be warned.

METHOD pbChooseAColourClick(o AS OBJECT , e AS EventArgs) AS VOID

  LOCAL oColDlg AS ColorDialog

  LOCAL drCol AS DialogResult

  LOCAL oMyCol AS Color

  //oColDlg:AnyColor := TRUE

  oColDlg := ColorDialog{}

  oColDlg:Color := Color.Red

  drCol := oColDlg:ShowDialog(SELF)

  oMyCol := oColDlg:Color

  SELF:ortbSubCats:BackColor := oMyCol

RETURN

Please note that the code does not have to be in an event handler method, but probably often will be. Obviously in my working code example I have a push button called "pbChooseAColour". As well as a line of code to set the event handler - as shown below.

SELF:opbChooseAColour:Click += EventHandler{ SELF , @pbChooseAColourClick() }

If you are unfamiliar with events and event handlers then see the Wiki articles on these to help explain the situation.

PHH: Oct. 2007
========================================================================================================================


Font

The Font Dialog Box is one of a handful which are provided in .NET to make life easy for the developer.

There are others like Color, FolderBrowser, OpenFile and SaveFile, as well as those to do with printing and page control.

The FontDialog class is easy to use but may well need the combined use of the ColorDialog class to allow user selection of more than a basic colour of the font to be used.

The dialog box itself provide a drop down list facility for 16 basic colours for the font itself.

Usually termed foreground colour for most Form controls.

I suggest you find out how to use the Color Dialog Box too, as it will increase your flexibility in coding.

The code below opens a Font dialog box with a default color of Olive from the 16 available.

A default font may also be specified using the Font property. Yuo need to consult the SDK for more information on this class.

It may be wise to also checkout the Font class.

LOCAL drCol AS DialogResult

LOCAL oMyCol AS Color

LOCAL oFontDlg AS FontDialog

LOCAL oMyFont AS Font

oFontDlg := FontDialog{}

oFontDlg:Color := Color.Olive

oFontDlg:ShowColor := TRUE

drCol := oFontDlg:ShowDialog(SELF)

oMyCol := oFontDlg:Color

oMyFont := oFontDlg:Font

SELF:ortbSubCats:Font := oMyFont

SELF:ortbSubCats:ForeColor := oMyCol

If you don't wish the user to choose a colour then make the SHowColor property FALSE.

You will need to experiment a bit more with this class than some of the other Dialog Box classes, as it has a few more options that can be helpful to the developer and user, if coded correctly.

Look into limiting the size with MinSize and MaxSize, as well as possibly restricting the font set to a certain type.

Now checkout other Wiki articles on Dialog Boxes.

PHH Oct.2007

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


Folder

The Folder Browser Dialog Box is similar to the OpenFile and SaveFile boxes. View the articles in Wiki on these too.

Although the code supplied below is enough to get you up and running with the FolderBrowser class, you should consult the Framework 2.0 SDK for more information on its use.

LOCAL oFBD AS FolderBrowserDialog

oFBD := FolderBrowserDialog{}

oFBD:ShowNewFolderButton := TRUE

oFBD:ShowDialog()

It is possible to not allow the user to make new folders by changing :-

oFBD:ShowNewFolderButton := FALSE

Checkout the RootFolder and SelectedPath properties.

You may wish to view other Wiki articles on Font, Colour, and printing related Dialog Boxes.

PHH Oct.2007

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


Files

A number of Dialog Boxes are provided in .NET and two useful ones are OpenFileDialog and SaveFileDialog.

{ Do not use the FileDialog class as it an abstract class used by the other two !}

The code below will get you going an allow you to select a file which is returned for you to use in your code as you so wish.

LOCAL oOFD AS OpenFileDialog

LOCAL cName AS STRING

oOFD := OpenFileDialog{}

oOFD:InitialDirectory := "C:\VIDE78\"

oOFD:Filter := "Program files (*.PRG)|*.PRG |Text files (*.txt)| *.txt |All files (*.*)| *.*"

oOFD:FilterIndex := 3

oOFD:CheckFileExists := TRUE

oOFD:ShowDialog()

cName := oOFD:FileName

oOFD := NULL_OBJECT

MessageBox.Show(SELF,cName,"Chosen File :")

There are a few things to notice about this code. You may provide a starting, or initial, directory (now termed folder).

You may provide one or a few filter patterns with discription and format seoarated by a "|" (said bar). Also, each pattern is also seoarated by a bar.

The filter pattern which is applied is chosen or set, by the filter index property. In this case 3 for the All Files choice.

The Check Files means that a check is made to see if the input filename exists. try this out for yourself.

When the filename is returned on closure of the Dialog Box it is there to be used by the developer as he/she wishes.

For this reason you may also wish to change the title on the Dialog Box - since you may not be going to open the file (or files) but are just using the Dialog Box to allow file "selection".

Try as follows :-

oOFD:Title := "My Choose a File :"

Now consult the Framework 2.0 SDK for the OpenFileDialog class to see what else may be done.

Below is some code to get you started for the FileSaveDialog Box.

Remember to consult the SDK and see how this can be improved and adapted to suit your needs.

For instance you may wish to check out the RestoreDirectory and OverwritePrompt properties.

LOCAL oSFD AS SaveFileDialog

oSFD := SaveFileDialog{}

oSFD:InitialDirectory := "C:\VIDE78\"

oSFD:Filter := "Program files (*.PRG)|*.prg |Text files (*.txt)| *.txt |All files (*.*)| *.*"

oSFD:FilterIndex := 3

oSFD:Title := "Save My Chosen File :"

oSFD:ShowDialog()

cName := oSFD:FileName

oSFD := NULL_OBJECT

For more information on other useful Dialog Boxes see articles in Wiki (and "Pearls") on those for Font, Color, FolderBrowser and printing and printers control.

PHH Oct.2007

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

Hi again,

Here are a couple of images to help you get going with using the two available Dialog boxes for opening and saving files.

The simple lines of code necessary to display these are shown on the image.

Please note however, you need to use the Framework 2.0 SDK documentation to find out more about both setting them up correctly before showing, as well as accessing information provided after user interaction.

All of us needs to get familiar with dipping into the SDK, it is not enough to have sample Vulcan code to guide us as there are usually a lot more options for properties and methods which need to be checked out !

Seem other eNote in "Pearls" on Dialogs related to printing and printers - these are a bit more complex, so enjoy the simplicity of these file related tools :-)

Phil.
=========================================================================================

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

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


Print

There are three Dialog Boxes related to printers and printing.

Each of these three needs to be supplied with a document before they can be used, either at all, without error, or meaningfully!

Although the one called "Print" appears to stand on its own and allows a printer to be selected, it does need to have a document supplied to be of real use.

If it is not given a genuine document object, then a number of features will be unavailable.

Windows users will be quite familiar with this dialog box as it is frequently used by all.

The printer is selected from a drop down list and then its properties may be set also.

These include orientation, color settings, quality, and paper size.

The other interacte parts of the PrintDialog Box allow page range settings to be made, and number of copies to be set.

LOCAL oPD AS PrintDialog

oPD := PrintDialog{}

oPD:Document := oBasicDoc

oPD:ShowDialog()

oPD := NULL_OBJECT

The PrinterSettings are obtained from the PrintDocument object - above it is oBasicDoc.

The changes made through the Dialog Box are applied directly to the PrinterSettings and the developer does not need to do any coding for this.

Typically, a developer handling his/her own printing may well display the Print Dialog Box first, then the Page Setup Dialog box.

The third type, the PrintPreview Dialog Box is called typically by the print event handler.

Look at other Wiki articles on the PageSetup and PrintPreview diakog boxes.

I suggest you consult the SDK documentation for Framework 2.0 on these classes too.

If you are intending to do your own printing in any way, you will need to use these three dialog boxes to get best results as far as a user is concerned.

Printing is not difficult, BUT, it is very fiddly and time consuming to set up correctly.

Once you have a printer / document class of your own then life becomes much easier, and the sky is your limit.

You can then print address labels, letters, reports, the lot.

PHH Oct.2007

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

Hi again,

There are three useful Dialog boxes available from the system which can greatly aid developers wishing to handle printing.

These are Print, PageSetup and PrintPreview - Dialog boxes.

The code related to these is in the image, but please realise that two of these need to have a document object before they can do their "stuff" !

The PageSetup dialog actually gave me an error box when it had not be given a document. It was possible to continue however.

Hope this helps those wishing to get into printing. It is very rewarding, BUT, a little tricky :-)

I will provide more information on printing documents in Wiki and "Pearls" later on. For now it is sufficient to say that the document given to the PrintPreview dialog was completely done from "home grown" Vulcan code.

Good Luck !
Phil.
===================================================================================================================================

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

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

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

Consult the section on Printing and "BasicDocument" class to see how the letter is printed.

You may print all your own material using the class and methods provided, including laser labels.


Section 25 - GUI form controls(IV)

In this section on DataTables, and the DataGridView control in WinForms, we will refer to a demo application I developed after reading the appropriate section in Paul Piko's book.

"Vulcan.net at Warp Speed" is the title and it is a must read for anyone starting Vulcan. You will notice from the names of data variables and objects that the "warp" had an influence on what I was doing at the time.

I am sure that there will be many more who gain inspiration and guidance from Paul's text. I will just say thanks to Paul for pointing me in this direction.

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

The above sample output can be used to explain the major separate parts to the demo application.

Part 1 :-

The data to be used is held in a data file (DBF format) on disk. So the first part of the application is where the data is read from disk, and put into a few data tables. The data tables are held as a data 'set'.

Each table in the full working application has over ten thousand records, and about 18 to 20 fields from the 117 in the original DBF.

You may notice some strange patient names, and this is because the names are fictitious and made up by some "not very clever" code of mine. So we get a Miss with a name like Ben ! However, at least the data is not confidential in any way :-0) And, serves the purpose nicely.

Part 2 :-

The second part is where one of the DataTables from the set may be applied to the DataGridView control for display. Some simple controls are provided on the form to allow the user to move between one table and another.

Although it takes a little while to create and fill a data table it takes no time at all to apply it to the grid.

Part 3 :-

You will notice from the above image that the cells are coloured in two different ways. So it is possible to colour and high-light the individual cells depending on their row and column, as well as the content of the cells themselves. The colouring shown above is simply red gradient for not active customer patients, and yellow gradient for those making orders - "active". We can look in more detail about other methods later.

It has to be realised that the colour and pattern of the display of the DataGridView control is done at the time the "CellPainting" takes place, and this is obviously event driven. It also needs to be pointed out that it is easy to get some "wrong" logic in your code for this painting process, such that little, or nothing, is shown on the control after painting. I know, I was there yesterday, can take ages to sort out !

For this reason it is good to have a way to easily switch back to see just the standard data display without the fancy gizmos. See the first image in the DataGridView sub-section for such a sample output.

Part4 :-

Part four is the part of the application which allows the user to change the order of the table being shown on screen. So we will look at what is behind the buttons for sorting names and setting filters.

Part 5 :-

Finally, as part five, we will see how making selection of a cell in a row can be used to launch an information, or edit screen, for that particular patient being selected.

As an extension exercise for those who find all this easy, they can add further controls so that the shading, colouring, and high-lighting pattern can be changed by a click of the mouse !

This actually would be a nice extension to the project.

I would like to point out that it is not the intended scope of ClickStartVDN to cover direct editing of cell data and writing back to the DBF source of all "dirty" cells. The topic of data binding is huge and forms the basis of individual books on the market at the moment. I have just purchased one recently. Once you get the basics of Vulcan and dotNet coding then revisit this topic for some further fun !

PHH Feb. 2008


DataTable & sets

Part 1 :-

We need to create a DataTable, or three, from the original data on disk. This example uses a simple DBF file.

Each data table will be put into a DataSet, from where they may be used to apply to the DataGridView control when required.

Basically, a new DataSet is instantiated, and each new table created is added to the set in turn. As the DBF is processed to make a new table, row objects are formed, and then added to the table being created.

Let us look at the code to cover these 'activities'.

PROTECT oWarpSet AS DataSet

At a class level we have a variable which will hold all the data tables we create. This is shown above.

PROTECT TableInFocus,DBF4Tables AS STRING

PROTECT MaxRow AS LONG

We will also have a class variable to hold the name of the data table being viewed, as well as the name of the DBF on disk, which is being read in at run time.

The other class variable holds the maximum row for the table in focus.

In order to make the demo application 'feel' better to the user, I have written the code to only create the required table (one of three) when the user requires to view it. Once the table exists in memory, it is not re-created unless the user chooses to do so. Otherwise it just shows the existing table with the required name.

Let us see how we code the push button 'Populate Grid' , which gets data to make the first of the three tables.

METHOD pbPopulateGridButtonClick(oControl AS OBJECT , e AS System.EventArgs) AS VOID

LOCAL Result AS DialogResult

LOCAL cTableName AS STRING

IF ! SELF:TableCheck("PatPersonal")

SELF:PopulateGrid()

ELSE

Result := MessageBox.Show(SELF,"The Table has already been constructed - Refresh ?","EXISTS !",MessageBoxButtons.YesNo, MessageBoxIcon.Question)

IF Result = DialogResult.Yes

//--------------------- need to remove existing table first ! -----------------------

SELF:RemoveWarpTable1("PatPersonal")

cTableName := "temporary_blank"

oWarpGridView:DataSource := oWarpSet:Tables[cTableName]

Application.DoEvents()

//----------------- will blank if not exist ----------------

SELF:PopulateGrid()

ENDIF

ENDIF

RETURN

You will notice that the method PopulateGrid() is only called if there is either no table with this name in memory, or the user wishes to over-write the table - we may call this a refresh usually.

So it is PopulateGrid() wich we need to look at :-

METHOD PopulateGrid() AS VOID

LOCAL aFields AS ARRAY

LOCAL cTableName AS STRING

cTableName := "PatPersonal"

MessageBox.Show(SELF,"Click to populate the grid !")

aFields := {1,2,3,4,5,6,7,46,8,9,10,11,12,13,14,15,17,18}

SELF:CreateWarpTable1(SELF:DBF4Tables, aFields, cTableName)

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

SELF:TableInFocus := "PatPersonal"

oWarpGridView:Hide()

oWarpGridView:DataSource := oWarpSet:Tables[SELF:TableInFocus]

oWarpGridView:Show()

RETURN

Okay, this does not look too bad, all we are doing is supplying a name for our table, and a long list of field numbers which relate to the fields positions in the DBF which is to be used.

Note that the fields relate to patient personal details in the data file - hence the name of the table. Also notice that they may be in any order we like.

Once we have passed the three parameters to the CreatWarpTablel() method <sorry about spelling> we simply need to display the new table.

Pointing the correct data table at the DataGridView object allows us to display what we are after, and hide() & show() make it look OK for the user too.

Finally, we need to see what is required in the CreateWarpTable() method, and although it is longer, it is not any more difficult to understand. So lets see what we need to do below :-

METHOD CreateWarpTable1(cDBF AS STRING, aFields AS ARRAY, cTableName AS STRING) AS VOID

RDDSETDEFAULT("DBFCDX")

Cursor.Current := Cursors.No

//-------------- display Hour Glass --------------

LOCAL oWD AS DBServer

LOCAL cFilePath, cText, cFieldName AS STRING

LOCAL WarpTableN AS DataTable

LOCAL oRow AS DataRow

LOCAL i, nFields, k, nLen, nStep AS LONG

LOCAL nStepCount, nRecs AS LONG

WarpTableN := DataTable{cTableName}

SELF:oWarpSet:Tables:Add(WarpTableN)

cFilePath := "c:\VDNUK\"+cDBF

oWD := DBServer{cFilePath}

oWD:GoTop()

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

SELF:ResetProgressBar()

nLen := oWD:RecCount

nStep := INTEGER(nLen/100)

nStepCount := 0

nFields := ALen(aFields)

FOR i := 1 UPTO nFields

k := aFields[i]

cFieldName := oWD:FIELDNAME(k)

WarpTableN:Columns:Add( DataColumn{cFieldName} )

NEXT // i

SELF:MaxRow := -1

DO WHILE ! oWD:EOF

oRow := WarpTableN:NewRow()

FOR i := 1 UPTO nFields

k := aFields[i]

cFieldName := oWD:FIELDNAME(K)

oRow[ cFieldName ] := oWD:FIELDGET(K)

NEXT

WarpTableN:Rows:Add(oRow)

SELF:MaxRow +=1

oWD:Skip(+1)

nStepCount +=1

IF nStepCount > 127

nStepCount := 0

oprgbWarpDataView:Value +=1

ENDIF

ENDDO

oprgbWarpDataView:Value := 100

Application.DoEvents()

SELF:ResetProgressBar()

Application.DoEvents()

oWD:Close()

oWD := NULL_OBJECT

Cursor.Current := SELF:Cursor

//------------------ display arrow -------------------

RETURN

Lets take this a step at a time. Don't let the code for the progress bar confuse the issue.

It adds a fair bit of code just to make things look nice for the user :-)

First we need to create a new data table with the name supplied as a parameter.

The new table is added to the data set, even before it has its rows added to it.

The first major part of making the table is the loop which goes through the fields and acquires the name for each from the DBF.

The name of each field then becomes the name of the column which is added to the table under construction.

Now that we have the names of the columns in the new table we need to fill the table with relevant data.

So the next loop goes through each record required from the DBF and looks at the fields which are needed.

For each record read in, a new 'row' object is instantiated. The contents of only the required fields are got and added to the row object.

At the end of the loop, when the row building is complete, the row is then added to the table itself, and a row pointer undated.

Remember a few lines are to control the progress bar - here called :- "oprgbWarpDataView" - my naming system is "object called progress bar for the WarpDataView Form".

So apart from resetting the progress bar , closing the DBF, and then resetting the cursor, we have done the job.

You may like to look up changing the cursor in another section to see how this produces a nice effect for the user.

Earlier in the event handler for the push button you may have noticed a line such as :-

SELF:RemoveWarpTable1("PatPersonal")

Well, all this does is call the short method which removes the existing table from the data set, called "PatPersonal". Let us look at this code now :-

METHOD RemoveWarpTable1(cTableName AS STRING) AS VOID

Cursor.Current := Cursors.No

//-------------- display Hour Glass --------------

SELF:oWarpSet:Tables:Remove(cTableName)

Cursor.Current := SELF:Cursor

//------------------ display arrow -------------------

RETURN

The cursor change is probably unnecessary as the whole process is very quick, so it is one line only which is required.

The good news is that the other event handlers which deal with the clicking (pushing) of the radio buttons, are extremely similar.

All that they do is provide a different data table name and a different selection of fields from the DBF.

And, the RemoveTable() method works for them too as it only requires a table name.

Before we finish this sub-section, there is one other method we need to study and that is the "TableCheck" method. This simply reports if a table exists with the name supplied.

METHOD TableCheck(cName AS STRING) AS LOGIC

LOCAL oDTC AS DataTableCollection

LOCAL lResult AS LOGIC

oDTC := oWarpSet:Tables

IF oDTC:Contains(cName)

//MessageBox.Show("Yes the table exists !")

lResult := TRUE

ELSE

//MessageBox.Show("No the table does NOT exist !!!")

lResult := FALSE

ENDIF

RETURN lResult

So now that we have a set of data tables the DataGridView control may display these, so that the user can start viewing and selecting as they require.

Go and look at how the user interacts with the grid in the section called "Sorting and Filtering".

PHH Feb. 2008


DataGridView - control

Let us take a look at the DataGridView control in WinForms, and see how it may be used to do interesting things.

The example shown here is just a basic one to wet your appetite and to get you to want more - so you will look into Data Binding and many other related things, more deeply.

Let us start with an image of a working application. See below for a sample output :-

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

In my working example I am displaying a data table with about eighteen fields, and thousands of rows. The data table has been made from a DBF on disk, and this is covered in the previous section on DataTables.

The DataGridView object shown has a set of three such DataTables, and any one may be displayed by using the radio buttons at top right of the Form.

It takes a little while to create and fill each data table the first time, but then displaying, and switching from one to another is very quick indeed.

We will see bellow how the cells in each row respond to the click event and display the MessageBox above - notice it has picked up the unique reference number of the patient of the row being selected by the click.

We will also see later, how another record edit screen is displayed to allow view and editing of the whole patient record.

The DataGridView control is placed on the blank for from the ToolBox and the resulting code generated by the IDE is shown below - this is part of the InitializeForm() method.

SELF:oWarpGridView := DataGridView{}

SELF:oWarpGridView:Name := "WarpGridView"

SELF:oWarpGridView:Location := Point{ 0 , 0 }

SELF:oWarpGridView:Size := Size{ 880 , 456 }

SELF:oWarpGridView:TabIndex := 1

SELF:oWarpGridView:Anchor := AnchorStyles.Top + AnchorStyles.Bottom + AnchorStyles.LEFT + AnchorStyles.RIGHT

SELF:oWarpGridView:Dock := DockStyle.None

SELF:Controls:Add(SELF:oWarpGridView)

Notice that the control has been anchored on all sides of the Form so that it grows and shrinks as the Form is re-sized.

In the class definition we have a DataSet object as below :-

PROTECT oWarpSet AS DataSet

In the Constructor method I have the following :-

oWarpSet := DataSet{}

as well as setting the read only property of the control itself :-

//------------ do not allow editing ! -------------

SELF:oWarpGridView:ReadOnly := TRUE

In the PopulateGrid method we simply need to create the data table with the name "PatPersonal" and then make it the table to be displayed in the grid control.

SELF:TableInFocus := "PatPersonal"

oWarpGridView:Hide()

oWarpGridView:DataSource := oWarpSet:Tables[SELF:TableInFocus]

oWarpGridView:Show()

Look at the previous section for creating the data table.

For now, lets us see how the DataTables are kept together in a set, and how we can switch between these.

LOCAL WarpTableN AS DataTable

WarpTableN := DataTable{cTableName}

SELF:oWarpSet:Tables:Add(WarpTableN)

PHH Feb. 2008.


Painting Cells - patterns

Once we have suitable data in a table, and this data table applied to an existing grid control, we can decide on ways to have it coloured to aid the user.

It may be that all records over-due are coloured or shaded in one way and the rest another. This way records needing attention can be seen.

Lets us see how we can do this in our code.

Below is a couple of lines I have in the SetMyEventHandlers() method I use to set the whole group of the handlers for the application Form.

//--------------------- screen Grid painting ---------------------

SELF:oWarpGridView:CellPainting += DataGridViewCellPaintingEventHandler{ SELF , @WarpGridViewCellPainting() }

So there is a method called WarpGridViewCellPainting() which is run each time a cell needs to be painted. Obviously, there must be a related event which asks for all the displayed cells to be painted.

At this point I must say that I found out how to do this sort of control "painting" by copying Paul's example, and then modifying it. So all credit should go to Paul for this one - any errors are mine :-)

Below is the most basic cell painting method and it produces a result like that seen at the beginning of the DataGridView sub-section.

METHOD WarpGridViewCellPainting( sender AS OBJECT, e AS DataGridViewCellPaintingEventArgs ) AS VOID

LOCAL rect AS Rectangle

/* ----------------- try these ! ------------------

e:AdvancedBorderStyle

e:CellBounds

e:CellStyle

e:ClipBounds

e:ColumnIndex

e:ErrorText

e:FormattedValue

e:Graphics

e:Handled

e:Paint()

e:Value

e:State

e:RowIndex

e:PaintParts

e:PaintContent()

e:PaintBackground()

*/

rect := e:CellBounds

e:Paint(rect, DataGridViewPaintParts.All)

RETURN

As you will notice the method as it is does very little, and mostly there are lines to remind me what is available for coding, as far as properties and methods of 'e'.

These can also be seen from the intellisense list in VIDE :-

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

Okay, now we have a basic method let us do something more interesting with the details.

Before we get stuck into doing anything fancy let me say that we need to make sure we only colour or shade the cells which make up the data section of the table or control.

So we need to check on whether the cell index is greater than zero and also if the row index is greater than zero and less than the row count minus two. This way we don't pattern the borders.

Also, quite importantly, we need to make sure that the highlighted cell (from mouse click selection) is coloured in a different way to the rest of our chosen pattern and not lost in our desire to be colourful or fancy.

IF e:ColumnIndex >= 0

rect := e:CellBounds

nRow := e:RowIndex

nCol := e:ColumnIndex

IF nRow >= 0 .and. nRow <= oWarpGridView:RowCount-2

IF ! _and(e:State, DataGridViewElementStates.Selected) == DataGridViewElementStates.Selected

//-------------------------- finds if cell is high-lighted -----------------------------

oDGVC := oWarpGridView:Item["patactive",e:RowIndex]

oContent := oDGVC:Value

cText := oContent:ToString()

lActive := FALSE

IF cText:Contains("T")

lActive := TRUE

ENDIF

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

IF !lActive

lgb := LinearGradientBrush{rect, Color.Red, Color.LightBlue, 0}

ELSE

lgb := LinearGradientBrush{rect, Color.Yellow, Color.LightBlue, 0}

ENDIF

e:PaintBackground(rect,TRUE)

e:Graphics:FillRectangle(lgb, rect)

e:PaintContent(rect)

e:Handled := TRUE

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

lgb:Dispose()

ENDIF

ENDIF

ENDIF

I have to admit that although I know what Paul's code is doing with the test to see if the cell is high-lighted, I don't really understand just what is is doing. However, that does not stop us from using it to good effect.

Basically, the code is being applied to cells inside the column and row bounds which are not high-lighted. Other than this we apply what fancy pattern, shading, and colour we choose.

You will notice that the code I have written is looking to see if the content of the "PatActive" cell for the row of the cell being painted is true or false. This way "active" patients get coloured with a linear gradient brush which goes from yellow to blue. (If you need to look up brushes look at the graphic section - 33.)

Notice that when we apply the colour it is to the background only, and for the full size of the cell - a rectangle determined by the 'e:CellBounds' property.

Again, don't worry if you don't quite understand all the details of this, just get the code to work for you and be able to make adjustments so that you gain the effect you require. This is just what I did from paul's original coding.

The live output from this code can be seen in the short introduction to this section.

Going a step further :-

What we can do now is show how to change this code to get some different effects fro easy viewing on the eye !

Let us first look at Paul's original pattern and style :-

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

The code for this is shown below :-

IF e:ColumnIndex >= 0

IF e:RowIndex %2 == 0

IF ! _and(e:State, DataGridViewElementStates.Selected) == DataGridViewElementStates.Selected

//-------------------------- finds if cell is high-lighted -----------------------------

rect := e:CellBounds

lgb := LinearGradientBrush{rect, Color.Red, Color.LightBlue, 0}

//lgb := LinearGradientBrush{rect, Color.Yellow, Color.LightBlue, 0}

e:PaintBackground(rect,TRUE)

e:Graphics:FillRectangle(lgb, rect)

e:PaintContent(e:CellBounds)

e:Handled := TRUE

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

lgb:Dispose()

ENDIF

ENDIF

ENDIF

Although this looks impressive, and certainly stimulated me to go on and try other things out, it could be more helpful to indicating what our data in view actually 'means'.

Instead of every other line being coloured, we could say lets colour rows which contain post code fields with Cardiff post codes. That is, beginning with "CF".

Let us see how that will look - first the code and then the output. We will also change the gradient brush from red to yellow.

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

Well, this appears to work as required so let us view the code section responsible.

IF ! _and(e:State, DataGridViewElementStates.Selected) == DataGridViewElementStates.Selected

//-------------------------- finds if cell is high-lighted -----------------------------

oDGVC := oWarpGridView:Item["pcode",e:RowIndex]

oContent := oDGVC:Value

cText := oContent:ToString():TRIM()

IF cText = "CF"

lgb := LinearGradientBrush{rect, Color.Red, Color.LightBlue, 0}

ELSE

lgb := LinearGradientBrush{rect, Color.Yellow, Color.LightBlue, 0}

ENDIF

e:PaintBackground(rect,TRUE)

e:Graphics:FillRectangle(lgb, rect)

e:PaintContent(rect)

e:Handled := TRUE

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

lgb:Dispose()

ENDIF

Lets make a final change to get a clearer effect and then take heed of a warning !

If we change the code as shown below, we then only colour the rows which have Cardiff post codes.

IF ! _and(e:State, DataGridViewElementStates.Selected) == DataGridViewElementStates.Selected

//-------------------------- finds if cell is high-lighted -----------------------------

oDGVC := oWarpGridView:Item["pcode",e:RowIndex]

oContent := oDGVC:Value

cText := oContent:ToString():TRIM()

lgb := LinearGradientBrush{rect, Color.Yellow, Color.LightBlue, 0}

IF cText = "CF"

e:PaintBackground(rect,TRUE)

e:Graphics:FillRectangle(lgb, rect)

e:PaintContent(rect)

ELSE

e:Paint(rect, DataGridViewPaintParts.All)

//--------- standard 'default' ----------

ENDIF

e:Handled := TRUE

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

lgb:Dispose()

ENDIF

Lets us see what this does to the look of the output :-

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

Only the Cardiff post code rows are coloured with the gradient brush the rest are painted in a standard 'default' way.

However, although data table one looks fine, the output for the others looks like that shown below :-

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

This appears to happen because we are not handling in our code that fact that there is not a 'PCode' column in tables two and three.

You may like to see if you can do suitable code tests for this and then for any field NOT existing get the cell to be painted in the standard 'default' way as in the code above.

Good Luck.

We could just as easily change this so that patients younger than 20 are shown in one colour, those over 60 another, and those in between some third colour.

In fact it would appear we could do a whole lot more is we so wish, now that we can examine the cell content for a specified cell in the row in question.

I think it is time to leave this now and let you go on to greater things. Let us just thank Paul again for his stimulating ideas in his "Warp Speed" publication.

Thanks Paul :O)

PHH Feb. 2008


Sorting and Filtering - the display

Filtering :-

When it comes to sorting and filtering the data presented in the data table in focus, life couldn't be easier.

There is a lot of functionality built into both the DataGridView control, and the DataTable, for the developer to tap into.

If we start by looking at the very short method called "SetDefaults" we may get a clue as to what can be done.

METHOD SetDefaults() AS VOID

oWarpSet:Tables[SELF:TableInFocus]:DefaultView:Sort := ""

oWarpSet:Tables[SELF:TableInFocus]:DefaultView:RowFilter := ""

RETURN

Here, when called, the "SetDefaults" method is effectively clearing the sort and filter conditions set on the data table.

If calling this method clears our sort and filter settings on the table being viewed, let us look at how to set some conditions for the user to have available at a click of a mouse.

METHOD pbFilterInactiveButtonClick(oControl AS OBJECT , e AS System.EventArgs) AS VOID

SELF:FilterInactive()

RETURN

METHOD FilterInactive() AS VOID

oWarpSet:Tables[SELF:TableInFocus]:DefaultView:RowFilter := "PATACTIVE = 'FALSE' "

RETURN

This at first looks a little bit over-complicated, BUT, my style is to have the "action" code in a different method to the event handler.

This really means we only need one line of code to set the table filter condition where only inactive patients are displayed on screen, in the table.

The operation is very quick in real time. Obviously the opposite filter "FilterActive" is simply based on :-

oWarpSet:Tables[SELF:TableInFocus]:DefaultView:RowFilter := "PATACTIVE = 'TRUE' "

Finally, on the filtering side of things, the "Default" push button just calls the "SetDefault" method at the top of this page - which removes the filter condition currently applied, AND, any sort condition.

If you so wish you could have a button called "Remove Filter" which just did as it says, and did not change the sort applied. It is all a matter of what the developer wishes to achieve.

METHOD pbDefaultButtonClick(oControl AS OBJECT , e AS System.EventArgs) AS VOID

SELF:SetDefaults()

RETURN

Obviously, if we can set filter conditions as above it is not difficult to see that we have a wide scope to apply lots of different filters. And in fact could have an interactive approach where the user selects a column, or field, and supplies the value at run time. This could be applied as the condition. However, before we get ahead of ourselves, let look at sorting the table in view.

Sorting :-

Now that we have got to grips with filtering data in our table, lets see what can be done with sorting it.

The "Sort Last + First" push button sorts the data into order by patient's last name plus first name - as is quite commonly required in applications.

METHOD pbSortLastNFirstButtonClick(oControl AS OBJECT , e AS System.EventArgs) AS VOID

SELF:SortLastNFirst()

RETURN

METHOD SortLastNFirst() AS VOID

oWarpSet:Tables[SELF:TableInFocus]:DefaultView:Sort := "SURNAME asc,FIRSTNAMES asc"

RETURN

Once again you can see that the sort "condition" is applied to the data table and not the displaying "grid".

At this point we have to see that the column headers in the grid control itself, allow the user to click and have an order displayed, either ascending of descending. An indicator arrow shows what has been clicked, that is which column is displaying the sort and in which direction.

I suspect that simply the grid control is setting a table sort for just the column clicked, and alternates from Ascending ti Descending, depending what is set on already.

Please be aware however, that this is an alphabetic sort so dates and figures may not do as you require.

Remembering that "ClickStartVDN" is just to get VOers, and other developers, "going" with Vulcan.net then I think we have covered enough to interest and stimulate.

It may well prove too, that the sort can be disabled as a "grid" property - so column header clicking would not function, however, the push buttons we have would still do the trick.

Good luck with your sorting and filtering projects !

PHH Feb. 2008


Selecting Rows - sub form display

In the working demo application I have made using the DataGridView control, I have included some, but not a lot, of code which responds to user selection within the grid cells.

In my method "SetMyEventHandlers()" - where I prefer to manage the event handlers myself and not have them sprinkled throughout the InitializeForm method - I have the following code :-

//------------------ Cell Selection by Double Click ------------------

SELF:oWarpGridView:CellDoubleClick += DataGridViewCellEventHandler{ SELF, @WarpGridViewCellDoubleClick() }

So the code can be made to respond to the user double clicking with the mouse, within the grid area displayed.

When any cell is selected by double clicking, the row is found and this is used to find the unique reference for the patient, see an example screen shot :-

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

Forgive my random data saying Pippa is a "guy", but at least we have the correct reference - 10012 !

As you will see this is then used to seek in the DBF for the correct record. The following was put in place when there were issues with the indexing for DBFs but it could go now that they appear to work fine.

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

Finally, there is a new Form to display the found record as shown below :-

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

Obviously the method "WarpGridViewCellDoubleClick()" is used to handle the double click event. Let us take a look at what I have included there.

METHOD WarpGridViewCellDoubleClick(oControl AS OBJECT , e AS DataGridViewCellEventArgs) AS VOID

LOCAL oDGVC AS DataGridViewCell

LOCAL nCol, nRow AS LONG

LOCAL cText, cText1 AS STRING

LOCAL oContent AS OBJECT

LOCAL oPatForm AS PatientDetails

Cursor.Current := Cursors.WaitCursor

//---------------- display hour glass -----------------

oDGVC := oWarpGridView:CurrentCell

nRow := oDGVC:RowIndex

nCol := oDGVC:ColumnIndex

oContent := oDGVC:Value

oDGVC := oWarpGridView:Item["patunref",nRow]

oContent := oDGVC:Value

RDDSETDEFAULT("DBFCDX")

LOCAL oWD AS DBServer

LOCAL cFilePath, cDBF, cMyValue AS STRING

LOCAL nMyValue, nLen, nStep AS LONG

LOCAL lFound AS LOGIC

LOCAL dMyDate AS DATE

setexclusive(TRUE)

cDBF := SELF:DBF4Tables

cFilePath := "c:\VDNUK\"+cDBF

oWD := DBServer{cFilePath}

oWD:SetIndex("c:\VDNUK\patanon32.cdx")

oWD:SetOrder("cpatref")

oWD:GoTop()

cText := oContent:ToString():TRIM()

cText := ALLTRIM(cText)

cText1 := ">> "+cText+" <<"

cMyValue := cText

nMyValue := VAL(cText)

dMyDate := CTOD("26/11/1928")

cText := "My value is :- "+NTrim(nMyValue)

MessageBox.Show(SELF,cText)

lFound := FALSE

IF oWD:Seek(cMyValue)

cText := "Seek and yee shall find ...."

MessageBox.Show(SELF,cText)

lFound := TRUE

ELSE

cText := "Sought but no finding !"

MessageBox.Show(SELF,cText)

ENDIF

IF lFound

oPatForm := PatientDetails{}

oPatForm:Start(nMyValue, oWD)

oPatForm:ShowDialog()

oPatForm := NULL_OBJECT

ENDIF

oWD:Close()

oWD := NULL_OBJECT

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

Cursor.Current := SELF:Cursor

//------------------ display arrow -------------------

RETURN

Most of the code is pretty straight forward and does not require explanation.

Obviously, this is just a simple example of what may be done using a DataGridView control and much more needs to be looked into as far as data binding is concerned, and editing the data in the cells directly, and much much more.

But it is a start, and it looks and feels nice.

I will leave you to add the part which allows you to edit the data in the edit Form, write it back to DBF and reflect the changes in the cell contents.

An interesting development would be to allow cell editing and now and then - or at time of closure - detect all the "dirty" cells (those that have changed) and write each and every one back into the DBF.

As a further extension you could use which of the columns has been clicked in, to do something different for each of these. This very much depends on your data.

All fun for you to follow up :o)

PHH Feb. 2008


FullWorking Code

CLASS WarpForm INHERIT Form

PROTECT oprgbWarpDataView AS ProgressBar

PROTECT opbDefault AS Button

PROTECT opbFilterInactive AS Button

PROTECT opbFilterActive AS Button

PROTECT opbSortLastNFirst AS Button

PROTECT opbPopulateGrid AS Button

PROTECT oOKButton AS Button

PROTECT opbHelpInfo AS Button

PROTECT oWarpGridView AS DataGridView

PROTECT oGroupBox1 AS GroupBox

PROTECT orbProfAdv AS RadioButton

PROTECT orbGP AS RadioButton

PROTECT orbPersonal AS RadioButton

// User code starts here (DO NOT remove this line) ##USER##

PROTECT oWarpSet AS DataSet

PROTECT TableInFocus,DBF4Tables AS STRING

PROTECT MaxRow AS LONG

PROTECT oMyTT AS ToolTip

CONSTRUCTOR()

SUPER()

SELF:InitializeForm()

RDDSETDEFAULT("DBFCDX")

//------------ do not allow editing ! -------------

SELF:oWarpGridView:ReadOnly := TRUE

SELF:orbPersonal:Checked := TRUE

SELF:Text := "Warp Data View 1 : Patient Personal : Names Fictitious !"

SELF:SetMyEventHandlers()

oWarpGridView:Cursor := Cursors.Hand

oWarpSet := DataSet{}

SELF:DBF4Tables :="patanon32.dbf"

SELF:ResetProgressBar()

SELF:SetMyToolTips()

oMyTT:Active := TRUE

RETURN

METHOD ResetProgressBar() AS VOID

oprgbWarpDataView:@@Step := 1

oprgbWarpDataView:Minimum := 0

oprgbWarpDataView:Maximum := 100

oprgbWarpDataView:Value := 0

RETURN

METHOD SetMyEventHandlers() AS VOID

SELF:opbPopulateGrid:Click += EventHandler{ SELF , @pbPopulateGridButtonClick() }

SELF:opbSortLastNFirst:Click += EventHandler{ SELF , @pbSortLastNFirstButtonClick() }

SELF:opbFilterActive:Click += EventHandler{ SELF , @pbFilterActiveButtonClick() }

SELF:opbFilterInactive:Click += EventHandler{ SELF , @pbFilterInactiveButtonClick() }

SELF:opbDefault:Click += EventHandler{ SELF , @pbDefaultButtonClick() }

SELF:opbHelpInfo:Click += EventHandler{ SELF , @pbHelpInfoButtonClick() }

//--------------------- screen Grid painting ---------------------

SELF:oWarpGridView:CellPainting += DataGridViewCellPaintingEventHandler{ SELF , @WarpGridViewCellPainting() }

//-------------- Radio Button Events -------------

SELF:orbPersonal:GotFocus += EventHandler{ SELF , @rbPersonalGotFocus() }

SELF:orbProfAdv:Click += EventHandler{ SELF , @rbProfAdvClick() }

SELF:orbGP:Click += EventHandler{ SELF , @rbGPClick() }

//------------------ Cell Selection by Double Click ------------------

SELF:oWarpGridView:CellDoubleClick += DataGridViewCellEventHandler{ SELF, @WarpGridViewCellDoubleClick() }

RETURN

METHOD pbHelpInfoButtonClick(oControl AS OBJECT , e AS System.EventArgs) AS VOID

SELF:DisplayHelpInformation()

RETURN

METHOD DisplayHelpInformation() AS VOID

LOCAL cText, cMess, cCaption, cR2 AS STRING

cR2 := CRLF+CRLF

cText := ""

cCaption := "HELP & Information : Data Grid View :"

cMess := "You may create up to three data tables using this demo app."+cR2

cMess += "The progress bar is linked to the DBF records being read and used."+cR2

cMess += "The first table 'personal' may be refreshed, or recreated."+cR2

cMess += "The column 'caption click' causes sorting by alphabetic content."+cR2

cMess += "Double clicking a grid cell will display an edit screen for the patient."+cR2

cMess += "The edit screen is not yet linked to any subsequent actions."+cR2

cMess += "Hover over each form control to see where Tool Tips may be set."+cR2

cMess += "I used the Handy Tips file supplied to code these TT's."+cR2

MessageBox.Show(SELF, cMess, cCaption, MessageBoxButtons.OK, MessageBoxIcon.Asterisk)

RETURN

METHOD SetMyToolTips() AS VOID

LOCAL oColF, oColB AS Color

oColF := Color.Yellow

oColB := Color.BlueViolet

oMyTT := ToolTip{}

oMyTT:AutoPopDelay := 5000

oMyTT:InitialDelay := 800

oMyTT:ReshowDelay := 350

oMyTT:IsBalloon := TRUE

oMyTT:BackColor := oColB

oMyTT:ForeColor := oColF

//_wait(oMyTT:BackColor:ToString())

//_wait(oMyTT:ForeColor:ToString())

//_wait(oColF:ToString())

//_wait(oColB:ToString())

/*oMyTT:BackColor := Color.Red

oMyTT:ForeColor := Color.Yellow */

oMyTT:SetToolTip(SELF:oprgbWarpDataView, "Represents the DBF records being read and processed.")

oMyTT:SetToolTip(SELF:opbDefault, "Restores the table in focus to its original state - after creation.")

oMyTT:SetToolTip(SELF:opbFilterActive, "Applies a filter to ONLY show 'active' patients.")

oMyTT:SetToolTip(SELF:opbFilterInactive, "Applies a filter to display 'inactive' patients.")

oMyTT:SetToolTip(SELF:opbPopulateGrid, "Create, or Re-Create, the 'personal' data table.")

oMyTT:SetToolTip(SELF:opbHelpInfo, "Gives some more detail about general use.")

oMyTT:SetToolTip(SELF:oOKButton, "What do you think this does ? :-)")

oMyTT:SetToolTip(SELF:opbSortLastNFirst, "Orders the table in focus by Surname, then Firstname.")

oMyTT:SetToolTip(SELF:orbGP, "Creates / Displays the GP format table.")

oMyTT:SetToolTip(SELF:orbProfAdv, "Creates / Displays the GP format table.")

oMyTT:SetToolTip(SELF:orbPersonal, "ONLY displays the GP format table.")

oMyTT:SetToolTip(SELF:oGroupBox1, "YES, you may even have a tool tip on a group box !!!")

//oprgbWarpDataView

/*oMyTT:BackColor := Color.Red

oMyTT:ForeColor := Color.Yellow */

//oMyTT:Dispose()

RETURN

METHOD TableCheck(cName AS STRING) AS LOGIC

LOCAL oDTC AS DataTableCollection

LOCAL lResult AS LOGIC

oDTC := oWarpSet:Tables

IF oDTC:Contains(cName)

//MessageBox.Show("Yes the table exists !")

lResult := TRUE

ELSE

//MessageBox.Show("No the table does NOT exist !!!")

lResult := FALSE

ENDIF

RETURN lResult

METHOD InitializeForm() AS VOID

// IDE generated code (please DO NOT modify)

SELF:Name := "WarpForm"

SELF:SuspendLayout()

SELF:Location := Point{100,100}

SELF:ClientSize := Size{1042,494}

SELF:Text := "Warp Data View 1 :"

SELF:oprgbWarpDataView := ProgressBar{}

SELF:oprgbWarpDataView:Name := "prgbWarpDataView"

SELF:oprgbWarpDataView:Location := Point{ 472 , 464 }

SELF:oprgbWarpDataView:Size := Size{ 408 , 23 }

SELF:oprgbWarpDataView:@@Step := 1

SELF:oprgbWarpDataView:RightToLeft := RightToLeft.Yes

SELF:oprgbWarpDataView:TabIndex := 9

SELF:Controls:Add(SELF:oprgbWarpDataView)

SELF:opbDefault := Button{}

SELF:opbDefault:Name := "pbDefault"

SELF:opbDefault:Location := Point{ 384 , 464 }

SELF:opbDefault:Size := Size{ 75 , 23 }

SELF:opbDefault:Text := "Default"

SELF:opbDefault:TabIndex := 6

SELF:opbDefault:Anchor := AnchorStyles.Bottom + AnchorStyles.Left

SELF:Controls:Add(SELF:opbDefault)

SELF:opbFilterInactive := Button{}

SELF:opbFilterInactive:Name := "pbFilterInactive"

SELF:opbFilterInactive:Location := Point{ 288 , 464 }

SELF:opbFilterInactive:Size := Size{ 88 , 23 }

SELF:opbFilterInactive:Text := "Filter Inactive"

SELF:opbFilterInactive:TabIndex := 5

SELF:opbFilterInactive:Anchor := AnchorStyles.Bottom + AnchorStyles.Left

SELF:Controls:Add(SELF:opbFilterInactive)

SELF:opbFilterActive := Button{}

SELF:opbFilterActive:Name := "pbFilterActive"

SELF:opbFilterActive:Location := Point{ 208 , 464 }

SELF:opbFilterActive:Size := Size{ 75 , 23 }

SELF:opbFilterActive:Text := "Filter Active"

SELF:opbFilterActive:TabIndex := 4

SELF:opbFilterActive:Anchor := AnchorStyles.Bottom + AnchorStyles.Left

SELF:Controls:Add(SELF:opbFilterActive)

SELF:opbSortLastNFirst := Button{}

SELF:opbSortLastNFirst:Name := "pbSortLastNFirst"

SELF:opbSortLastNFirst:Location := Point{ 104 , 464 }

SELF:opbSortLastNFirst:Size := Size{ 96 , 23 }

SELF:opbSortLastNFirst:Text := "Sort Last + First"

SELF:opbSortLastNFirst:TabIndex := 3

SELF:opbSortLastNFirst:Anchor := AnchorStyles.Bottom + AnchorStyles.Left

SELF:Controls:Add(SELF:opbSortLastNFirst)

SELF:opbPopulateGrid := Button{}

SELF:opbPopulateGrid:Name := "pbPopulateGrid"

SELF:opbPopulateGrid:Location := Point{ 0 , 464 }

SELF:opbPopulateGrid:Size := Size{ 96 , 23 }

SELF:opbPopulateGrid:Text := "Populate Grid"

SELF:opbPopulateGrid:TabIndex := 2

SELF:opbPopulateGrid:Anchor := AnchorStyles.Bottom + AnchorStyles.Left

SELF:Controls:Add(SELF:opbPopulateGrid)

SELF:oOKButton := Button{}

SELF:oOKButton:Name := "OKButton"

SELF:oOKButton:Location := Point{ 936 , 464 }

SELF:oOKButton:Size := Size{ 104 , 24 }

SELF:oOKButton:Text := "&Close"

SELF:oOKButton:Click += EventHandler{ SELF , @OKButtonClick() }

SELF:oOKButton:TabIndex := 0

SELF:oOKButton:Anchor := AnchorStyles.Bottom + AnchorStyles.Right

SELF:oOKButton:Dock := DockStyle.None

SELF:Controls:Add(SELF:oOKButton)

SELF:opbHelpInfo := Button{}

SELF:opbHelpInfo:Name := "pbHelpInfo"

SELF:opbHelpInfo:Location := Point{ 912 , 192 }

SELF:opbHelpInfo:Size := Size{ 104 , 23 }

SELF:opbHelpInfo:Text := "HELP / Info."

SELF:opbHelpInfo:TabIndex := 8

SELF:Controls:Add(SELF:opbHelpInfo)

SELF:oWarpGridView := DataGridView{}

SELF:oWarpGridView:Name := "WarpGridView"

SELF:oWarpGridView:Location := Point{ 0 , 0 }

SELF:oWarpGridView:Size := Size{ 880 , 456 }

SELF:oWarpGridView:TabIndex := 1

SELF:oWarpGridView:Anchor := AnchorStyles.Top + AnchorStyles.Bottom + AnchorStyles.Left + AnchorStyles.Right

SELF:oWarpGridView:Dock := DockStyle.None

SELF:Controls:Add(SELF:oWarpGridView)

SELF:oGroupBox1 := GroupBox{}

SELF:oGroupBox1:Name := "GroupBox1"

SELF:oGroupBox1:SuspendLayout()

SELF:oGroupBox1:Location := Point{ 896 , 8 }

SELF:oGroupBox1:Size := Size{ 128 , 160 }

SELF:oGroupBox1:Text := "Select Table :"

SELF:oGroupBox1:TabIndex := 7

SELF:oGroupBox1:Anchor := AnchorStyles.Top + AnchorStyles.Right

SELF:Controls:Add(SELF:oGroupBox1)

SELF:orbProfAdv := RadioButton{}

SELF:orbProfAdv:Name := "rbProfAdv"

SELF:orbProfAdv:Location := Point{ 24 , 112 }

SELF:orbProfAdv:Size := Size{ 80 , 24 }

SELF:orbProfAdv:Text := "Prof Adv :"

SELF:orbProfAdv:TabIndex := 2

SELF:orbProfAdv:Anchor := AnchorStyles.Top + AnchorStyles.Right

SELF:oGroupBox1:Controls:Add(SELF:orbProfAdv)

SELF:orbGP := RadioButton{}

SELF:orbGP:Name := "rbGP"

SELF:orbGP:Location := Point{ 24 , 72 }

SELF:orbGP:Size := Size{ 80 , 24 }

SELF:orbGP:Text := "GP :"

SELF:orbGP:TabIndex := 1

SELF:orbGP:Anchor := AnchorStyles.Top + AnchorStyles.Right

SELF:oGroupBox1:Controls:Add(SELF:orbGP)

SELF:orbPersonal := RadioButton{}

SELF:orbPersonal:Name := "rbPersonal"

SELF:orbPersonal:Location := Point{ 24 , 32 }

SELF:orbPersonal:Size := Size{ 80 , 24 }

SELF:orbPersonal:Text := "Personal :"

SELF:orbPersonal:TabIndex := 0

SELF:orbPersonal:Anchor := AnchorStyles.Top + AnchorStyles.Right

SELF:oGroupBox1:Controls:Add(SELF:orbPersonal)

SELF:oGroupBox1:ResumeLayout()

SELF:ResumeLayout()

RETURN

METHOD WarpGridViewCellDoubleClick(oControl AS OBJECT , e AS DataGridViewCellEventArgs) AS VOID

LOCAL oDGVC AS DataGridViewCell

LOCAL nCol, nRow AS LONG

LOCAL cText, cText1 AS STRING

LOCAL oContent AS OBJECT

LOCAL oPatForm AS PatientDetails

Cursor.Current := Cursors.WaitCursor

//---------------- display hour glass -----------------

oDGVC := oWarpGridView:CurrentCell

nRow := oDGVC:RowIndex

nCol := oDGVC:ColumnIndex

oContent := oDGVC:Value

oDGVC := oWarpGridView:Item["patunref",nRow]

oContent := oDGVC:Value

RDDSETDEFAULT("DBFCDX")

LOCAL oWD AS DBServer

LOCAL cFilePath, cDBF, cMyValue AS STRING

LOCAL nMyValue, nLen, nStep AS LONG

LOCAL lFound AS LOGIC

LOCAL dMyDate AS DATE

setexclusive(TRUE)

cDBF := SELF:DBF4Tables

cFilePath := "c:\VDNUK\"+cDBF

oWD := DBServer{cFilePath}

oWD:SetIndex("c:\VDNUK\patanon32.cdx")

oWD:SetOrder("cpatref")

oWD:GoTop()

cText := oContent:ToString():Trim()

cText := AllTrim(cText)

cText1 := ">> "+cText+" <<"

cMyValue := cText

nMyValue := Val(cText)

dMyDate := CToD("26/11/1928")

cText := "My value is :- "+NTrim(nMyValue)

MessageBox.Show(SELF,cText)

lFound := FALSE

IF oWD:Seek(cMyValue)

cText := "Seek and yee shall find ...."

MessageBox.Show(SELF,cText)

lFound := TRUE

ELSE

cText := "Sought but no finding !"

MessageBox.Show(SELF,cText)

ENDIF

IF lFound

oPatForm := PatientDetails{}

oPatForm:Start(nMyValue, oWD)

oPatForm:ShowDialog()

oPatForm := NULL_OBJECT

ENDIF

oWD:Close()

oWD := NULL_OBJECT

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

Cursor.Current := SELF:Cursor

//------------------ display arrow -------------------

RETURN

METHOD rbPersonalGotFocus(oControl AS OBJECT , e AS System.EventArgs) AS VOID

SELF:Text := "Warp Data View 1 : Patient Personal : Names Fictitious !"

SELF:TableInFocus := "PatPersonal"

oWarpGridView:Hide()

oWarpGridView:DataSource := oWarpSet:Tables[SELF:TableInFocus]

oWarpGridView:Show()

RETURN

METHOD rbProfAdvClick(oControl AS OBJECT , e AS System.EventArgs) AS VOID

LOCAL cTableName AS STRING

LOCAL aFields AS ARRAY

IF orbProfAdv:Checked

cTableName := "PatProfAdv"

oWarpGridView:DataSource := oWarpSet:Tables[cTableName]

Application.DoEvents()

//----------------- will blank if not exist ----------------

IF ! TableCheck(cTableName)

MessageBox.Show(SELF,"Creating Table for first time !", cTableName+" :")

aFields := {1,2,3,6,7,8,45,21,22,23,24,25,26,27,28,29,30,31,32,33,34}

SELF:CreateWarpTable1("patanon32.dbf", aFields, cTableName)

Application.DoEvents()

ENDIF

SELF:Text := "Warp Data View 1 : Patient Prof. Advisor : Names Fictitious !"

SELF:TableInFocus := cTableName

oWarpGridView:Hide()

oWarpGridView:DataSource := oWarpSet:Tables[SELF:TableInFocus]

oWarpGridView:Show()

Application.DoEvents()

ENDIF

RETURN

METHOD rbGPClick(oControl AS OBJECT , e AS System.EventArgs) AS VOID

LOCAL cTableName AS STRING

LOCAL aFields AS ARRAY

IF orbGP:Checked

cTableName := "PatGP"

oWarpGridView:DataSource := oWarpSet:Tables[cTableName]

Application.DoEvents()

//----------------- will blank if not exist ----------------

IF ! TableCheck(cTableName)

MessageBox.Show(SELF,"Creating Table for first time !", cTableName+" :")

aFields := {1,2,3,6,7,8,45,21,22,23,24,25,26,27,28,29,30,31,32,33,34}

SELF:CreateWarpTable1("patanon32.dbf", aFields, cTableName)

Application.DoEvents()

ENDIF

SELF:Text := "Warp Data View 1 : Patient GP : Names Fictitious !"

SELF:TableInFocus := cTableName

oWarpGridView:Hide()

oWarpGridView:DataSource := oWarpSet:Tables[SELF:TableInFocus]

oWarpGridView:Show()

Application.DoEvents()

ENDIF

RETURN

METHOD WarpGridViewCellPainting( sender AS OBJECT, e AS DataGridViewCellPaintingEventArgs ) AS VOID

LOCAL rect AS Rectangle

LOCAL lgb AS LinearGradientBrush

LOCAL oRow AS DataRow

LOCAL cText AS STRING

LOCAL oContent AS OBJECT

LOCAL oDGVC AS DataGridViewCell

LOCAL lActive AS LOGIC

LOCAL nRow, nCol AS LONG

/* ----------------- try these ! ------------------

e:AdvancedBorderStyle

e:CellBounds

e:CellStyle

e:ClipBounds

e:ColumnIndex

e:ErrorText

e:FormattedValue

e:Graphics

e:Handled

e:Paint()

e:Value

e:State

e:RowIndex

e:PaintParts

e:PaintContent()

e:PaintBackground()

*/

//e:Paint(rect, DataGridViewPaintParts.All)

//RETURN

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

/*IF e:ColumnIndex >= 0

IF e:RowIndex %2 == 0

IF ! _and(e:State, DataGridViewElementStates.Selected) == DataGridViewElementStates.Selected

//-------------------------- finds if cell is high-lighted -----------------------------

rect := e:CellBounds

lgb := LinearGradientBrush{rect, Color.Red, Color.LightBlue, 0}

//lgb := LinearGradientBrush{rect, Color.Yellow, Color.LightBlue, 0}

e:PaintBackground(rect,TRUE)

e:Graphics:FillRectangle(lgb, rect)

e:PaintContent(e:CellBounds)

e:Handled := TRUE

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

lgb:Dispose()

ENDIF

ENDIF

ENDIF

RETURN */

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

/*IF e:ColumnIndex >= 0

rect := e:CellBounds

nRow := e:RowIndex

nCol := e:ColumnIndex

IF nRow >= 0 .and. nRow <= oWarpGridView:RowCount-2

IF ! _and(e:State, DataGridViewElementStates.Selected) == DataGridViewElementStates.Selected

//-------------------------- finds if cell is high-lighted -----------------------------

oDGVC := oWarpGridView:Item["patactive",e:RowIndex]

oContent := oDGVC:Value

cText := oContent:ToString()

lActive := FALSE

IF cText:Contains("T")

lActive := TRUE

ENDIF

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

IF !lActive

lgb := LinearGradientBrush{rect, Color.Red, Color.LightBlue, 0}

ELSE

lgb := LinearGradientBrush{rect, Color.Yellow, Color.LightBlue, 0}

ENDIF

e:PaintBackground(rect,TRUE)

e:Graphics:FillRectangle(lgb, rect)

e:PaintContent(rect)

e:Handled := TRUE

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

lgb:Dispose()

ENDIF

ENDIF

ENDIF */

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

IF e:ColumnIndex >= 0

rect := e:CellBounds

nRow := e:RowIndex

nCol := e:ColumnIndex

IF nRow >= 0 .and. nRow <= oWarpGridView:RowCount-2

IF ! _and(e:State, DataGridViewElementStates.Selected) == DataGridViewElementStates.Selected

//-------------------------- finds if cell is high-lighted -----------------------------

oDGVC := oWarpGridView:Item["pcode",e:RowIndex]

oContent := oDGVC:Value

cText := oContent:ToString():Trim()

lgb := LinearGradientBrush{rect, Color.Yellow, Color.LightBlue, 0}

IF cText = "CF"

//lgb := LinearGradientBrush{rect, Color.Red, Color.LightBlue, 0}

//lgb := LinearGradientBrush{rect, Color.Yellow, Color.LightBlue, 0}

e:PaintBackground(rect,TRUE)

e:Graphics:FillRectangle(lgb, rect)

e:PaintContent(rect)

ELSE

//lgb := LinearGradientBrush{rect, Color.Yellow, Color.LightBlue, 0}

e:Paint(rect, DataGridViewPaintParts.All)

ENDIF

//e:PaintBackground(rect,TRUE)

//e:Graphics:FillRectangle(lgb, rect)

//e:PaintContent(rect)

e:Handled := TRUE

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

lgb:Dispose()

ENDIF

ENDIF

ENDIF

RETURN

METHOD pbDefaultButtonClick(oControl AS OBJECT , e AS System.EventArgs) AS VOID

SELF:SetDefaults()

RETURN

METHOD pbFilterInactiveButtonClick(oControl AS OBJECT , e AS System.EventArgs) AS VOID

SELF:FilterInactive()

RETURN

METHOD pbFilterActiveButtonClick(oControl AS OBJECT , e AS System.EventArgs) AS VOID

SELF:FilterActive()

RETURN

METHOD pbSortLastNFirstButtonClick(oControl AS OBJECT , e AS System.EventArgs) AS VOID

SELF:SortLastNFirst()

RETURN

METHOD OKButtonClick(oControl AS OBJECT , e AS System.EventArgs) AS VOID

SELF:Close()

RETURN

METHOD pbPopulateGridButtonClick(oControl AS OBJECT , e AS System.EventArgs) AS VOID

LOCAL Result AS DialogResult

LOCAL cTableName AS STRING

IF ! SELF:TableCheck("PatPersonal")

SELF:PopulateGrid()

ELSE

Result := MessageBox.Show(SELF,"The Table has already been constructed - Refresh ?","EXISTS !",MessageBoxButtons.YesNo, MessageBoxIcon.Question)

IF Result = DialogResult.Yes

//--------------------- need to remove existing table first ! -----------------------

SELF:RemoveWarpTable1("PatPersonal")

cTableName := "temporary_blank"

oWarpGridView:DataSource := oWarpSet:Tables[cTableName]

Application.DoEvents()

//----------------- will blank if not exist ----------------

SELF:PopulateGrid()

ENDIF

ENDIF

RETURN

METHOD SetDefaults() AS VOID

oWarpSet:Tables[SELF:TableInFocus]:DefaultView:Sort := ""

oWarpSet:Tables[SELF:TableInFocus]:DefaultView:RowFilter := ""

RETURN

METHOD SortLastNFirst() AS VOID

//oWarpSet:Tables["patientDBF"]:DefaultView:Sort := "SURNAME desc"

oWarpSet:Tables[SELF:TableInFocus]:DefaultView:Sort := "SURNAME asc,FIRSTNAMES asc"

RETURN

METHOD FilterActive() AS VOID

oWarpSet:Tables[SELF:TableInFocus]:DefaultView:RowFilter := "PATACTIVE = 'TRUE' "

//oWarpSet:Tables[SELF:TableInFocus]:DefaultView:RowFilter := "PATACTIVE = '.T.' "

//oWarpSet:Tables["patientDBF"]:DefaultView:RowFilter := "PATUNREF > 2000"

RETURN

METHOD FilterInactive() AS VOID

//oWarpSet:Tables[SELF:TableInFocus]:DefaultView:RowFilter := "PATACTIVE = '.F.' "

oWarpSet:Tables[SELF:TableInFocus]:DefaultView:RowFilter := "PATACTIVE = 'FALSE' "

RETURN

METHOD PopulateGrid() AS VOID

LOCAL aFields AS ARRAY

LOCAL cTableName AS STRING

cTableName := "PatPersonal"

MessageBox.Show(SELF,"Click to populate the grid !")

aFields := {1,2,3,4,5,6,7,46,8,9,10,11,12,13,14,15,17,18}

SELF:CreateWarpTable1(SELF:DBF4Tables, aFields, cTableName)

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

SELF:TableInFocus := "PatPersonal"

oWarpGridView:Hide()

oWarpGridView:DataSource := oWarpSet:Tables[SELF:TableInFocus]

oWarpGridView:Show()

RETURN

METHOD CreateWarpTable1(cDBF AS STRING, aFields AS ARRAY, cTableName AS STRING) AS VOID

RDDSETDEFAULT("DBFCDX")

Cursor.Current := Cursors.No

//-------------- display Hour Glass --------------

LOCAL oWD AS DBServer

LOCAL cFilePath, cText, cFieldName AS STRING

LOCAL WarpTableN AS DataTable

LOCAL oRow AS DataRow

LOCAL i, nFields, k, nLen, nStep AS LONG

LOCAL nStepCount, nRecs AS LONG

WarpTableN := DataTable{cTableName}

SELF:oWarpSet:Tables:Add(WarpTableN)

cFilePath := "c:\VDNUK\"+cDBF

oWD := DBServer{cFilePath}

oWD:GoTop()

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

SELF:ResetProgressBar()

nLen := oWD:RecCount

nStep := Integer(nLen/100)

nStepCount := 0

nFields := ALen(aFields)

FOR i := 1 UPTO nFields

k := aFields[i]

cFieldName := oWD:FieldName(k)

WarpTableN:Columns:Add( DataColumn{cFieldName} )

NEXT // i

nRecs := 0

SELF:MaxRow := -1

DO WHILE ! oWD:EoF

oRow := WarpTableN:NewRow()

FOR i := 1 UPTO nFields

k := aFields[i]

cFieldName := oWD:FieldName(K)

//oRow[ cFieldName ] := AsString(oWD:FIELDGET(K))

oRow[ cFieldName ] := oWD:FIELDGET(K)

NEXT

WarpTableN:Rows:Add(oRow)

SELF:MaxRow +=1

nRecs +=1

IF nRecs > 698

EXIT

ENDIF

oWD:Skip(+1)

nStepCount +=1

IF nStepCount > 127

nStepCount := 0

oprgbWarpDataView:Value +=1

ENDIF

ENDDO

oprgbWarpDataView:Value := 100

Application.DoEvents()

SELF:ResetProgressBar()

Application.DoEvents()

oWD:Close()

oWD := NULL_OBJECT

Cursor.Current := SELF:Cursor

//------------------ display arrow -------------------

RETURN

METHOD RemoveWarpTable1(cTableName AS STRING) AS VOID

Cursor.Current := Cursors.No

//-------------- display Hour Glass --------------

SELF:oWarpSet:Tables:Remove(cTableName)

Cursor.Current := SELF:Cursor

//------------------ display arrow -------------------

RETURN

END CLASS

PPP Feb. 2008


Section 26 - moving from VO Functions

Moving Away from User Functions :-

( Note :- Thanks to Robert VdH for pointing out the static use below :- )

If we are to embrace the dotNet style and take it into our own Vulcan programming style, we need to find a way to keep our own useful VO functions, at the same time as giving up using functions.

If this sounds a bit 'Irish' <sorry for the English vernacular, and to my international colleagues :-) > I will explain what I mean below.

At many places in the dotNet framework we see static methods and this really is a way of having functions to use and not using functions.

The 'File.Exists' static method is just one in a group of methods in the File class.

It would appear that they are really just a group of previous functions put together under the "banner" of FILE.

So why don't we adopt the same approach and group our functions in the same way ?

The simple - without thought - way is to call the class "MyOldFunctions" as shown below :-

CLASS MyOldFunctions

STATIC METHOD RemoveSpaces(cText AS STRING) AS STRING

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

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

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

RETURN cResult

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

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

END CLASS

LOCAL cNoSpaces, cOrigText AS STRING

cOrigText := "spot the dog"

cNoSpaces := MyOldFunctions.RemoveSpaces(cOrigText)

As Robert points out we don't need to instantiate if we make the methods 'static'.

Don't forget to strongly type your functions while converting them. It may be a while since you wrote them :-)

Here is live and working example of my first attempt to make this work :-

As you can see it works fine. However, I still have some work to do to change over from VO functions to dotNet methods.

I have already changed one 'AllTrim()' but now need to change this quite old coding to remove At() and Stuff().

In fact studying it closely it looks as if lots can be done to 'modernise' it.

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

As you will notice, once the SDK is consulted to see what is on offer as "the tools" to manipulate strings, we can come down to a very simple method indeed.

Don't forget to check out all of these STRING methods as they may help you improve your old VO code dramatically too :-)

In the above example you may wonder why we have a static method at all. Time will sort this out I am sure.

Strategy :-

If you group similar functions into one class, and then a few "function" classes into a PRG, you will have them for many new Vulcan apps in the future.

Finally, in case one function calls another, don't forget that they are now methods of the same class so use the same syntax to call from within your new static methods.

This may be the time to radically review and change coding in each function code - as seen above :-) Don't miss the chance to do this, you won't regret it.

I strongly suggest you group your old functions into suitably named new classes (think carefully about this), and then put them into one PRG for use in all your apps.

That is unless you have lots and lots, in some sort of specialised way. Then you will have to work out your own strategy for managing them, and having them available at the time they are required.

Moving Away from VO Functions :-

In the past number of months it has started to feel "dated" when I use an older style VO function, say for example 'NTrim()', and so I have made a conscious effort to move to using one style only, that of dotNet.

So I intend to us 'ToString()' and many of the rich string class methods, as well as 'Conversion.Value()'. Once you try them out you quickly get accustomed to their use, and it also feels right too.

You will need to use ToSting() a lot as all "things" are objects, even integers. Some more complex objects have a sting form, so you need to know the name of the property which returns a string.

However, you need to remember that VO USUALs, and dotNet methods, do not mix ! So don't use USUAL data type either, when you have an option not to.

To get you going on this path, check out the properties and methods of the STRING class.

PHH December 2007.


Section 27 - moving from VO to Vulcan

I have waited some eight months or so before writing this section.

In that time I have looked at most of the basic elements of coding in Vulcan.NET and focused particularly on learning the new side of things - that is the dotNet features.

I have got a 'feel' for what is old, and should be avoided, and what is good and 'nice to use' of the new tools.

It now seems to me that there are a few "dos and don't" that I can recommend, and intend to stick to myself.

Since it is the rich side of the .NET features that has drawn me to make the effort to get into Vulcan, I intend to change my coding style to allow me to continue to use as many of the dotNet features as possible - BUT - by making it as easy for myself as I can :-)

Why have two ways of doing things when one will do fine ?

- Give up VO dynamic arrays

- don't use USUALs

- strong type always

- use object methods in place of functions (give up VO functions)

- when old VO arrays need to be used, use "aVO:Length" to find their length as this is an INT

- use Items:AddRange for listboxes of different descriptions instead of Items:Add

- use DWORD data type when the variable should never be negative

- don't mix INT and DWORD as I used to - without thought etc.

- always use Win Forms - till WPF is available - then move ASAP

- design move interaction with events into my GUIs

- try to get into, and use, drag and drop more

- except for small sting handling tasks use String Builder - to save much time

- when I can find a way to do it - move away from DBFCDX use too

- do all printing and printer handling with .NET tools

- I would even say that when it is possible to write all new code, it would make sense to set the compile switch so that all arrays are zero based

It has taken a lot of time and effort to put this list together so I hope it helps you as much as it will help me :-)

PHH December 2007.


Section 28 - Framework SDK documentation

For you to get the most from Vulcan.NET you need to use as much of the .NET framework as possible.

If we are to move over successfully to the benefits of the "new world" of programming we need to refer to the SDK documentation on a VERY regular basis.

Every developer and coder needs to be happy using the documentation. Such aids as ClickStart will not cover everything, they are just to give a flavour, and point you in the right direction.

Below is a genuine folder from what I use to do my demo apps, Vulcan development, and ClickStart coding.

As you can see I have up to nine icons which are for information provision. Of these, by far the most used is the Framework 2.0 SDK icon, and now that ClickStartVDN is filling out I also refer to that for working Vulcan syntax.

I suggest as early as possible you put together a folder similar to the one below.

PHH December 2007.


Section 29 - General Tips

If you every used in VO the little trick to trap the enter key at a form level by having a hidden default button which could then handle the required sequence of events afterwards - well it still works.

Now and again the user wishes to have the enter key cause an action, or even a sequence, depending on how many times the enter key has been pressed.

It is possible to set the default push button on a Windows Form to respond in the manner above.

You may even wish one thing to happen at the first press, another at the second, and a final thing at the third. And then go round again.

This is still possible with Win Forms, as it was with VO's GUI.

Personally, I used to hide a push button called "pbEnter" and have it the default for the form.

If you have not tried this before then it may come in handy sometime.


Changing the Cursor

It is useful to provide the user with more information about what may be happening "behind" a Windows Form, or to indicate what the control under the mouse pointer may do.

This can be easily done at certain times by changing the type (style or shape) of the cursor for the window in focus.

Cursor.Current := Cursors.No
{ display red circle with bar }

Cursor.Current := SELF:Cursor
{ display arrow }

(Every form and every control has a cursor property.)

It is difficult to show an image of this in action as the cursor is not displayed on a screen capture :o(

Notice that there is a cursor belonging to the form (SELF:Cursor) and also a "current" or temporary one.

These are different "things", but may be set to the same cursor type however.

The "current" cursor is only returned to an "arrow" if the WinForm cursor (SELF:Cursor) was an "arrow" before the change.

Notice if you wish the cursor to be different for the "DataGridView" control on my form, as opposed to the rest of the form then :-

oWarpGridView:Cursor := Cursors.Hand

will show a fingered "hand" pointing, but only when the mouse pointer is within the boundaries of the DataGridView control.

Some other styles are shown below :-

Cursors.WaitCursor
Cursors.Default / Cursors.Arrow
Cursors.Cross
Cursors.Help
Cursors.VSplit
Cursors.IBeam

See Framework 2.0 SDK documentation for all the rest.


Prevent Form Resizing

To prevent a form from being resized at run time use :-

SELF:FormBorderStyle := FormBorderStyle.FixedDialog

Put this in the Form Constructor() after :-

SELF:InitialisationForm()

If you don't stop resizing then make sure that all the form's controls are locked to a side, or top / bottom.

You MUST handle a 'user resize', or the results will be awful!


Clearing Windows Tasks

Developers in VO have been used in the past to using such statements as :-

DoEvents()

and the much older :-

ApplicationExec(EXECWHILEEVENT)

Well, in Vulcan you may need to use something similar for the same reasons as before :-)

Now we need to use :-

Application.DoEvents()


Your VO Functions in Vulcan

Many VO developers moving to Vulcan have a whole host of functions which they may wish to still use in Vulcan.

It is probably a good idea to check that when you copy them over you make changes to the code to take advantage of the richer features of the .NET classes.

Don't be boring and write them the same as they were. Use methods for string and integer objects, not the old VO functions.

You may also be able to use 'StringBuilder' in more places than you image too :-)

However, to keep with the spirit of .NET, where everything is on object with methods and/or properties, try doing the following.

Make a new class in Vulcan called "MyFunctions" and then add a method to this class for each of your converted functions.

Suppose there is a function called "RemoveSpaces(cText)" which is now a method of "MyFunctions". To use this in your Vulcan code try :-

LOCAL oMyOldFuncs AS MyFunctions

LOCAL cNoSpaces, cOrigText AS STRING

oMyOldFuncs := MyFunctions{}

cOrigText := "spot the dog"

cNoSpaces := oMyOldFuncs:RemoveSpaces(cOrigText)

Don't forget to strongly type your functions while converting them. It may be a while since you wrote them :-)

If you group similar functions into one class, and then a few "function" classes into a PRG then you have them for many new Vulcan apps.

Finally, in case one function calls another, don't forget that they are now methods of the same class so use :-

SELF:TheNextFunction(A,B,C ...)

PHH: Oct.2007

Note: If you make the methods STATIC in the class you don't even have to instantiate the class:

CLASS Foo

STATIC METHOD Bar

.

.

RETURN ..

END CLASS

FUNCTION Start AS INT

Foo.Bar()

RETURN 1

RvdH Oct. 2007


Pausing an Application

In order to get an application to appear to pause, or wait for a certain time period before continuing processing, use the following code :-

System.Threading.Thread.Sleep(1000)

1000 (milliseconds) gives a one second pause, and 5000 a five second pause, and so on.

This will work even without a 'USING' statement however the following works too :-

USING System.Threading

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

Thread.Sleep(1000)

An example of use in a Vulcan application running an external application is as follows :-

DO WHILE ! oProcess:HasExited

System.Threading.Thread.Sleep(1300)

ENDDO


Making a Form Modal

Sometimes it is useful to be able to make a user respond to a Form before the application moves on.

Such Forms are said to be "modal".

Forms which stay on screen while the application continues are said to be "modeless".

Which type of Form you use depends on the problem being solved.

To make your Form modal use :-

SELF:ShowDialog()

and to get a modeless Form use :-

SELF:Show()

To close both types of Form use :-

SELF:Close()


Passing Command Line Parameters

Passing Command Line Parameters to VDN Apps:

When a Vulcan.NET (VDN) application is launched, it is possible to pass a list of parameters separated by a space. The Start() function can then process these as explained in Paul Piko's book, and as shown in the VIDE basic start function Chris supplies for the user.

Let us look at such a start function below :-

[STAThreadAttribute];

FUNCTION Start( asCmdLine AS STRING[] ) AS INT

LOCAL nExitCode AS INT

LOCAL oForm AS BasicForm

Application.EnableVisualStyles()

Application.DoEvents()

oForm := BasicForm{}

Application.Run(oForm)

RETURN nExitCode

This "stub" provides a framework for both exit code passing as well as command line parameter passing. Exit codes are passed back to the operating system, whereas parameters are provided by the developer and passed on the launching command line.

If we were to run a VDN application called "ScreenPrinting" from within a VO program the following code lines may be used :-

cMyFolder := "Print2FileDotNET\"

cFolder := guWkTemp+cMyFolder

cFile := "Vulcan_Config_Simple.PHH"

cRunLineVulcan := [c:\VDNexamples\ScreenPrinting.EXE]+SPACE(1)+cfolder+SPACE(1)+cFile

oApp := App{}

oApp:Run(cRunLineVulcan)

oApp := NULL_OBJECT

So when we get into our start function in VDN we need to look at the object "asCmdLine" to see if it is empty, or to extract the data if it is not. The code sample below attempts to extract two strings from the command line, a folder name and a config filename. The code could be tidied up a little, but, it does work as it is if you provide the necessary other bits.

cFolder := ".... blank ...."

cConfig := ".... blank ...."

FOR i := 1 UPTO CmdLineArgs:Length

oLine := CmdLineArgs[i]

cText := oLine:ToString()

IF i = 1

cFolder := cText

ENDIF

IF i = 2

cConfig := cText

ENDIF

NEXT

cFolder := ALLTRIM(cFolder)

In my early developmental app I then had some code to see if two strings had been passed, and if not post a message on screen. It is possible to run this VDN app without input, but the code needs to find out this fact so it can handle the consequences.

Lets look at what may be done :-

LOCAL cHeading AS STRING

oShowRepSimple := MakeShowSimpleReport{cFolder, cConfig}

IF !oShowRepSimple:OpenReadConfigFile()

cText := "WARNING ... Can't open & read CONFIG file !"+CRLF+CRLF

cText += "Folder :- "+cFolder+CRLF

cText += "Config :- "+cConfig+CRLF

cText += CRLF+CRLF+"[ CAUSE -- Probably NOT Auto-Launched from a VO application !!! ]"

cHeading := "WARN : No PARAMETERS appear to be supplied :- "

nExitCode := 13

IF MessageBox.Show(cText, cHeading, MessageBoxButtons.OKCancel, MessageBoxIcon.Asterisk) = DialogResult.Cancel

RETURN nExitCode

ENDIF

ENDIF

The code provided above should be enough to give the developer a good start with writing his/her own code to allow a VO app (or other) to interface with a VDN app by providing command line information.


Application Running - Details

It is useful sometimes to be able to find out more about the application which is running.

The properties of the Application class may be used to to find out more, and have this detail available at a code level.

LOCAL cDetail AS STRING

cDetail := Application.ExecutablePath

MessageBox.Show(cDetail,"File and path of executable !")

//cDetail := Application.ProductName

//cDetail := Application.StartupPath

//cPathDetail := Application.CommonAppDataPath

It is suggested that you consult the Framework 2.0 SDK documentation for the Application class.

PHH Oct.2007


Returning an Exit Code

Returning and Exit Code to the Operating System :

The Start() function in a Vulcan.NET application can return INT, DWORD or VOID. Any other return type is illegal and will raise a compiler error.

The return value is passed back to the operating system as the exit code, which can be tested in a batch file, the ExitCode property of the Process class, etc

For example:

FUNCTION Start() AS INT

LOCAL retcode AS INT

...

retcode := 1

...

RETURN retcode

Note that while the ErrorLevel() runtime function is supported for compatiblity with VO, it doesn't do anything in VO so it doesn't do anything in Vulcan either.

Alternative :

If you wish to pass back more than what is possible above, try using the Clipboard and in this way you may pass back a long string.

The launching program / app could then examine the Clipboard to see what is available.

Yet another way would be to create a file - the information for what to call the file and where to put it could be done by passing parameters at run time. See the earlier sub-section on "Passing Command Line Parameters".


VO - Clipboard info from Vulcan

Enter topic text here.


String to Numeric

When looking for a .NET way of replacing the use of the Val() function in VO it is tempting to look at the String class amongst the whole host of useful methods available.

Well, don't - it took me a while to realise that what is required is the Conversion class.

LOCAL cAge AS STRING

LOCAL nAge AS LONG

cAge := "97"

nAge := Conversion.VAL(cAge)

There are three overloads to the method - Char, Object and String.

It is worth looking up this class in the SDK documentation to see details of these, as well as Oct(), Str(), Hex() and some other useful methods and overloads.

PHH December 2007.


Killing Off Bad Habits

We now have a chance in Vulcan to get one approach to looping using arrays. And, for the approach to be correct as far as the compiler is concerned.

For VO users, in the past ALen() was used to find the length of a VO dynamic array. Unfortunately for some :-) it returned a DWORD and not an integer.

Many people still used "i" as an iterator and declared this as type INT - however, this generated compiler warnings, so then folks turned off the warnings by applying the compiler switch.

"Going sideways" is a phrase which comes to mind.

We now have a chance to put things right, simply, and for good !

In Vulcan, both VO dynamic ARRAYs and System.Array (SA) namespace dotNet arrays have a property called "Length" and this returns an integer value.

So now we can still have "sexy" INTs in place of DWORDs, and not cause warnings.

The code below will do the trick for both types of array :-

LOCAL i AS INT

LOCAL iLen AS INT

iLen := aNames:Length

FOR i := 1 UPTO iLen

....

cName := aNames[i]

....

NEXT // i

PHH December 2007.


Mixing your SubStrings

Just a word of warning, to try and help.

I have just come across a compile problem, which stopped me in my tracks. It is quite simple in essence, but can throw you !

I am now trying to use .NET syntax whenever possible, so string handling is done using dotNet methods of the string class and not VO functions.

Using intellisense in VIDE, I came to requiring a substring and must have been offered "SubStr" - which was selected. I continued on the line to use Trim() and the line looked like this :-

cHead := cLine:SubStr(0,iPosn)

But this give a compiler error of :-

error VN4004: unknown function or method: 'STRING.SubStr(INT,INT)'

To the VOer moving across to Vulcan this can be difficult, as the eye runs over the code and can't easily see (spot) that it should read :-

cHead := cLine:SubString(0,iPosn)

Obviously, when you stand back, anyone can see SubStr is not a method of the .NET string class,but a VO function name.

You just need to be aware in this strange world of the coder - developer needing two heads !

PHH December 2007.


Form Control Box - remove !

Removing the Form's top right controls.

Sometimes you may wish to not let users close a form by clicking the red box with the cross.

See below for both the effect and the code which brings it about.

PHH December 2007.


Destructor Method

This is something new to VO folk.

In Vulcan we now have a Destructor method which can be seen as working the opposite of the Constructor.

It appears to be the last method to be called before an object is destroyed, or un-instantiated. For this reason it is a good place to do your final tidying up.

Close you data files and the like.

This is certainly a better place to do such tidying for objects based on WinForms, than fighting the events of query close and other such things.

I have not explored the Destructor as much as I would like as yet so will only make this general point for the moment.

A word of caution from one colleague suggested that for Forms the garbage collector may be missed by doing certain things after the closing of the Form.

We will have to see and explore this more.

PHH Jan. 2008


Array Indexes - zero & one !

The possibility that arrays in Vulcan may be Zero or One based needs some discussion :

On 12th February Don Caton wrote this in the news group :-

No, all arrays in Vulcan are 0-based at the IL level. The compiler just does a little trickery so that they're 1-based at the language level.

For example:

a[x]

is actually compiled as:

a[x-1]

If the array index is a literal value, then the compiler just subtracts 1 from the literal value.

Turning on the zero-based array option just stops the compiler from subtracting 1 from array offsets. Either way, the actual array is 0-based and therefore, compatible with the rest of the .NET world.

I'll add a note about this in the docs for the /az compiler option.

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

Now that I have done a variety of things with dotNet it would seem that some advice could be put in simplified terms to help the newcomer to Vulcan, and the guy who wants to write high level business solutions without going down too deep into the 'nuts and bolts' of the language.

VO had one based arrays, and so for compatibility this was provided in Vulcan. However, it does NOT translate into all arrays in Vulcan.NET being one based if the compiler switch is set that way.

Only user created arrays have a base of one, when the compiler switch is set that way. You need to be careful with how you "see this" as arrays made by the system are always zero based.

And, it isn't that black and white either. If you create and fill a dotNet 'standard' array (I got the term 'standard' from an SDK example) as one based, and then sort it with the ArraySort(aMyStandardArray), then the result needs to be used by your code written for a zero based array. Even though you appear to be using the same variable.

To see this in action with live code, view Section 2 on Data Structures - Arrays.

Obviously, any other array or collection created and passed back by the system for you to use in your code, will be zero base too.

The best advice is to say have a go, and get used to writing and testing your code carefully at first, wherever you have array bounds being used. Copy some of the working ("default" compiler settings) code in Click Start.

Having been into Vulcan for some nine months now it would seem easier to say - put the compiler switch so all arrays are zero based - BUT - how many of us have to go back to maintain VO code ? Either as VO or as transported to Vulcan. If this whole thing wasn't an issue then it would not be being discussed in the NG and I would not be writing about it here !

I say it is an issue to be addressed - BUT - it is not at all as much of a problem to me as I thought it would be.

I suspect that it is one of those things that we will have to live with and get used to for some time to come. I am sticking with one based arrays so that in VO and Vulcan I am consistent - and it seems easier to draft on to your coding style the zero based coding of dotNet and system related "stuff".

Good Luck ;-)

PHH March 2008.


Trapping Runtime Errors

While trying to come to terms with sorting and searching in the array section I stumbled over the fact that there seems to be an issue at the moment with the built-in comparer of the FLOAT type.

Look at the code and output below :-

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

At a guess it would appear that the sort method may be the cause of out problems. To check this out we can use the TRY / CATCH / END TRY code structure to trap the exception and see a message if that is the case.

The tip was given to me by Frank Maraite fron the news group - thanks Frank.

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

Well that seems quite useful, and I am sure we can apply this code in plenty of other places now we know what to do.

If the problem has anything to do with the built-in comparer for type FLOAT then it ought to cause another problem slightly further on in the code execution, at the BinarySearch() method.

Below is the code to trap the exception at the sort line, and then again at the search line. See how it looks :-

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

Obviously, for the execution to continue the we must not have a correctly sorted array, so the binary search would most likely not give a suitable result anyhow, BUT, at least we can test whether our idea about the built-in comparer is correct.

As you can see the execution does go on to the search line and a suitable message shows we still have a problem, so it is unlikely to be the sort itself which caused the first problem, and most likely the built-in comparer.

Problem is, by the time you read this Don will have corrected things and you will not be able to force this situation to see how to use the TRY / CATCH / END TRY code structure.

Still, at least you will get a correct result. Talking of which, let us see what we get if we let the execution go further.

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

As we suspected, our results leave a lot to be desired :-) No sort, no binary search, no correct result !

We have a useful extra technique to our repertoire however, and that's always good news.

Full Working Code :-

LOCAL aListNums AS ArrayList

LOCAL cText, cHead AS STRING

LOCAL nLen AS DWORD

LOCAL nResult AS INT //------ important for negative results of search ------

aListNums := ArrayList{}

aListNums:Add(3.33)

aListNums:Add(2.22)

aListNums:Add(1.11)

aListNums:Add(8.88)

aListNums:Add(6.66)

aListNums:Add(5.55)

TRY

aListNums:Sort()

CATCH e AS System.Exception

MessageBox.Show(e:Message, "Something Wrong Sort !")

END TRY

nLen := aListNums:Count

TRY

nResult := aListNums:BinarySearch(5.55)

CATCH e AS System.Exception

MessageBox.Show(e:Message, "Something Wrong Search !!")

END TRY

cText := ""

cHead := "Search Result :"

cText += "position is :- "+nResult:ToString()+e"\n\n"

cText += "number is :- "+aListNums[nResult]:ToString()+e"\n\n"

cText += e"[ zero based after sort! ]\n"

MessageBox.Show(cText, cHead)

PHH March 2008.


Section 30 - ToolTips

Setting and changing Tool Tips for Windows Forms is really easy, and can prove to be very flexible for the user.

The overall look of the application can be enhanced using Tool Tips, now in different and interesting ways.

First declare a ToolTip object in the "User Section" of the CLASS declaration for the Form.

CLASS ....... >> declaration

// User code starts here (DO NOT remove this line)  ##USER##

PROTECT oMyTT AS ToolTip

This next part is how I have done it in my demonstration apps to date. I am sure there are many other ways.

I make a class method called "SetMyToolTips()" - in this I set up what I wish to do and an example is shown below.

In the CONSTRUCTOR() I usually put something like :-

SELF:SetMyToolTips()

oMyTT:Active := TRUE

The method to "set" the ToolTips looks similar to :-

METHOD SetMyToolTips() AS VOID

  LOCAL oColF, oColB AS Color

  oColF := Color.Yellow

  oColB := Color.BlueViolet

  oMyTT := ToolTip{}

  oMyTT:AutoPopDelay := 5000

  oMyTT:InitialDelay := 800

  oMyTT:ReshowDelay := 350

  oMyTT:IsBalloon := TRUE         //------------- see Framework 2.0 SDK for other options --------------

  oMyTT:BackColor := oColB

  oMyTT:ForeColor := oColF

  oMyTT:SetToolTip(SELF:opbHelpInfo, "Gives some more detail about general use.")

  oMyTT:SetToolTip(SELF:oOKButton, "What do you think this does ? :-)")

  oMyTT:SetToolTip(SELF:opbSortLastNFirst, "Orders the table in focus by Surname, then Firstname.")

  oMyTT:SetToolTip(SELF:orbGP, "Creates / Displays the GP format table.")

  oMyTT:SetToolTip(SELF:orbProfAdv, "Creates / Displays the GP format table.")

  oMyTT:SetToolTip(SELF:orbPersonal, "ONLY displays the GP format table.")

  oMyTT:SetToolTip(SELF:oGroupBox1, "YES, you may even have a tool tip on a group box !!!")

RETURN

The ToolTips can then be turned off using :-

oMyTT:Active := FALSE

Once you get the hang of doing it in this simple manner you may realise that it could be more flexible than this, you could have levels of ToolTip help - Beginner, Standard & Advanced.

You could then have buttons, or menu options, to allow the user to choose which level they require - or indeed to just turn them off altogether !

I have not tried my next suggestion yet, BUT, it would appear to me that you could have more than one ToolTip object declared, and therefore have groups which belong to one and not the other.

In this way TTs for part of a Form may be turned on or off, separately to another part of the same Form.

Please someone, anyone, do some extra fun tests to see just what can be done with TTs in .NET - and let us all know via Wiki and "Pearls".

One group could even be yellow on blue, and the other red on yellow :-) You need XP32 to see this however (see Colourful TTs - was in Pearls too).


Section 31 - String Builder class

Hi "concatenating cousins",

>> USING System.Text <<

Just a short but important note about the dotNet StringBuilder class.

At first when I heard of this dotNet class I did not appreciate just how good a "tool" it would turn out to be.

Now that I have had a chance to use it in a few places, and see it in action, I am impressed enough to tell folk that "for this reason only" it is worth moving to Vulcan.NET - just to get StringBuilder.

Below (and attached) is a sample of code from a current demo about sorting ArrayLists. Once sorted, I wished to show the order had changed. I started off by just displaying the first twenty objects in the list - thinking of the time, and the delay to the demo in action. (There are 11,000 - eleven thousand - objects in the ArrayList).

However, once I tried the StringBuilder (SB) approach, and saw have fast it was, I now display all 11,000 of them in a RichTextBox, with fixed spaced font. (See image below.)

I have written the code for demo purposes, so that a check box can allow either SB printing, or the conventional string concatenation approach. On my reasonably fast Vista PC it takes a full ten seconds to do the task the VO way, and just the click of the mouse on the push button to achieve the same with SB - no obvious delay at all..

I just wish I had this available to me for my VO internal reporting in my network app.

If you have not yet tried SB give it a go. Don't just use it for what you think are the "biggish" jobs, use it whenever you need to process strings - which are not 'trivial' in nature.

I had to force myself to do similar, but am glad I did. Thanks to Don & Chris for this one :-)

METHOD DisplayFullList() AS VOID

LOCAL i, nLen AS LONG
LOCAL oSB, oSBL AS StringBuilder
LOCAL cText, cLine AS STRING

oSB := StringBuilder{}
cText := ""

nLen := aList:Count
IF ocbUseStringBuilder:Checked
FOR i := 0 UPTO nLen-1
oPatRec := (PatientRecord)aList[i]

oSBL := StringBuilder{}
oSBL:Append(oPatRec:Surname:Trim():PadRight(12))
oSBL:Append(oPatRec:Firstnames:Trim():PadRight(10))
oSBL:Append(oPatRec:PatRef:ToString():PadLeft(8)+Space(2))
oSBL:Append(oPatRec:DoB:ToString():PadRight(12))
oSBL:Append(oPatRec:PostCode:ToString():PadRight(10))
oSBL:Append(oPatRec:LastOrderNum:ToString())
oSBL:Append(CRLF)

oSB:Append( oSBL:ToString() )
NEXT // i
ortbSortOrders:Text := oSB:ToString()
ELSE
FOR i := 0 UPTO nLen-1
oPatRec := (PatientRecord)aList[i]

cLine := ""
cLine += oPatRec:Surname:Trim():PadRight(12)
cLine += oPatRec:Firstnames:Trim():PadRight(10)
cLine += oPatRec:PatRef:ToString():PadLeft(8)+Space(2)
cLine += oPatRec:DoB:ToString():PadRight(12)
cLine += oPatRec:PostCode:ToString():PadRight(10)
cLine += oPatRec:LastOrderNum:ToString()
cLine += CRLF

cText += cLine
NEXT // i
ortbSortOrders:Text := cText
ENDIF

RETURN

Just a final note, this demo also sorts the ArrayList into various orders as the image may indicate - well, each sort is only a blink too. Look at Pearls and Wiki articles on sorting ArrayLists - here I am using the IComparer approach, first given to me by Paul Piko.


Section 32 - String Formatting

Provided in dotNet for the developer are some useful and powerful ways of formatting strings for output.

I have only covered a part of what is available here, just to give you a flavour of what to expect.

Notice in order to get the country specific formats it is necessary to use a new namespace shown below.

Obviously, the CultureInfo class is what sets the necessary formats before use.

As well as numeric formatting, we have Date and Time, as well as enumeration and others.

I suggest once you have seen what is on offer here, and had a try for yourself, you return to the section and topic only when you require something a little different.

So remember, if you are starting to require more than the standard string output, then visit the Framework 2.0 SDK documentation after seeing if what you require is in these simple examples.

If you are really feeling brave the look at the IFormattable and IFormatProvider details

USING System.Globalization

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

Now that we have gained some confidence with the more straight forward numeric output, let us have a go at some dates and times.

To try these will will make a DateTime object which has the values of "now" for our testing.

One slight problem is that whereas above the 'case' of the format character was not "sensitive", (either upper or lower case produced the same results), we now have to be careful as this is not the situation for Dates and Times.

By changing the format character from "d" to "D" see what happens to the output :-

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

So now let us see, with some simple coding, just what else can be dome wit a DateTime 'enity'.

Take particular attention of the upper and lower case differences for the letters "d,f,g,t,and u".

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

Just to give you something to do I have not used the remaining three character combinations which produce some extra results.

Try for yourself "dddd, MMMM dd yyyy" then "ddd, MMM d \""'\""yy" finally "dddd, MMMM dd". Note there is a single quote embedded in the second group, with two pairs of double quotes too, so you may wish to consult the SDK documentation to see more clearly.

Consult the DateTime.ToString() method, and don't forget you need the Globalization namespace as in the first example above.

I think I have done my bit to help. I never liked this sort of formatting from the very early days with Hollerith statements, even though it is powerful.

If ugly is power, then <geez> these ain't have powerful !

I wish to return to my images and colour :-)


working code

LOCAL oCI AS CultureInfo

LOCAL cText, cHead AS STRING

LOCAL oDT AS DateTime

oCI := CultureInfo{"en-GB"}

cText := ""

cHead := "Date & Time Formatting :"

oDT := DateTime.Now

cText += "d : "+oDT:ToString("d",DateTimeFormatInfo.InvariantInfo)+CRLF

cText += "D : "+oDT:ToString("D",DateTimeFormatInfo.InvariantInfo)+CRLF

cText += "f : "+oDT:ToString("f",DateTimeFormatInfo.InvariantInfo)+CRLF

cText += "F : "+oDT:ToString("F",DateTimeFormatInfo.InvariantInfo)+CRLF

cText += "g : "+oDT:ToString("g",DateTimeFormatInfo.InvariantInfo)+CRLF

cText += "G : "+oDT:ToString("G",DateTimeFormatInfo.InvariantInfo)+CRLF

cText += "m : "+oDT:ToString("m",DateTimeFormatInfo.InvariantInfo)+CRLF

cText += "r : "+oDT:ToString("r",DateTimeFormatInfo.InvariantInfo)+CRLF

cText += "s : "+oDT:ToString("s",DateTimeFormatInfo.InvariantInfo)+CRLF

cText += "t : "+oDT:ToString("t",DateTimeFormatInfo.InvariantInfo)+CRLF

cText += "T : "+oDT:ToString("T",DateTimeFormatInfo.InvariantInfo)+CRLF

cText += "u : "+oDT:ToString("u",DateTimeFormatInfo.InvariantInfo)+CRLF

cText += "U : "+oDT:ToString("U",DateTimeFormatInfo.InvariantInfo)+CRLF

cText += "y : "+oDT:ToString("y",DateTimeFormatInfo.InvariantInfo)+CRLF

cText += "M/yy : "+oDT:ToString("M/yy",DateTimeFormatInfo.InvariantInfo)+CRLF

cText += "dd-MM-yy : "+oDT:ToString("dd-MM-yy",DateTimeFormatInfo.InvariantInfo)+CRLF

MessageBox.Show(cText, cHead)

LOCAL oCI AS CultureInfo

LOCAL cText, cHead AS STRING

LOCAL nLong AS LONG

oCI := CultureInfo{"en-GB"}

cText := ""

cHead := "Numeric Formatting :"

cText += "Currency : "+String.Format("{0:c}",2.5)+CRLF

cText += "Currency : "+String.Format("{0:c}",-2.5)+CRLF

cText += "Decimal : "+String.Format("{0:D5}",25)+CRLF

cText += "Scientific : "+String.Format("{0:e}",250000)+CRLF

cText += "Fixed Point : "+String.Format("{0:F2}",25)+CRLF

cText += "Fixed Point : "+String.Format("{0:f0}",25)+CRLF

cText += "General : "+String.Format("{0:G}",2.5)+CRLF

cText += "Number : "+String.Format("{0:N}",2500000)+CRLF

cText += "Hexadecimal : "+String.Format("{0:X}",250)+CRLF

cText += "Hexadecimal : "+String.Format("{0:x}",0xFFFF)+CRLF

MessageBox.Show(cText, cHead)


Section 33 - Graphics

Showing and explaining the features in the GDI+ available to us in modern Windows is quite a huge task, is all available features are to be demonstrated.

This is the topic for a full set of help notes in it own rights.

However, it is possible to cover some of the standard features required to do some useful graphics work, without getting too voluminous.

Most of what will be immediately useful to you is in the sub-section 'Some basic concepts'.

Since it is possible to learn some more useful dotNet techniques by following up Font Families, I have included this in a sub-section of its own.

Finding the fonts and families is similar in nature to finding the Windows printers.

PHH Jan. 2008


Some basic concepts

In this short section on Graphics we will cover output to screen and printer. We will cover Shapes, Lines, Pens, Brushes, Fonts, Colors and Text.

In GDI+ we say that Shapes, Lines and Text (SLT) are drawn. Shapes are made of lines (and curves) and these are drawn with a pen.

To draw lines with GDI+ you need to create a Graphics object and a Pen object. The Graphics object provides the methods that actually do the drawing, and the Pen object stores attributes, such as line color, width, and style.

You first need to realise that the topic of GDI+ in very extensive and one about which whole books are written. I suggest you buy such a book if you intend to do anything serious, or just for reference, I have :-)

Basic Ideas :- ( some points directly from the SDK documentation )

Before you can draw lines and shapes, render text, or display and manipulate images with GDI+, you need to create a Graphics object. The Graphics object represents a GDI+ drawing surface, and is the object that is used to create graphical images.

Screen or printer may be selected for "output".

On an instantiated graphics object, shapes, lines and text are drawn with specified parameters which control the "look" of the displayed item in question.

Pens have a color associated with them and an enumeration exists which allows easy access to a whole range of pens with a 'set' color.

The outline of a shape is drawn with the pen, but the body of a shape is filled using a brush. Only "closed" shapes can be filled.

Brushes can be of type solid, hatch, texture, linearGradient, or pathGradient. See these examples from the Framework 2.0 SDK documentation.

This can all seem a little confusing till you try out some examples to see what may be done, and what the result looks like.

Creating the Graphics surface :-

Screen :- Call the 'CreateGraphics' method of the form, or control, upon which you want to render graphics.

Printer :- If the output is to go to the printer then the methods of the graphics object of the 'PrintPageEventArgs' object needs to be used from the 'PrintPageEventHandler'.

See the sections on printing for this, and the 'ClickPrintVDN' notes available April 2008.

Shapes :- (rectangles, ellipses, pies, and paths)

Let us see what the code looks like to draw a shape on a Form, and what the result output looks like.

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

Obviously, if we make the major and minor axes of the ellipse equal we get a circle so we can easily improve our image as shown below :-

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

By changing the code slightly so we draw the ellipse rather than fill it, and use a pen instead of a brush we can get as follows :-

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

Before we move on to the next sub-section, lets us finally fill a rectangle, and use a rectangle object.

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

I will leave it up to you to find out about other shapes, like polygons, from the SDK and do your own fun testing. You know enough to do this from the working code above. Look at 'pies' and 'paths' too.

Brushes :- (solid, hatch, texture, linearGradient, and pathGradient)

In our examples above we have used a solid brush to fill the ellipse shapes and rectangle. However, there are many more with some interesting and useful properties.

Let us now try to repeat one of the above examples with a couple of different hatched brushes.

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

Go to the SDK to have a look at the 'HatchStyle' enumeration. There are quite a few, and with some interesting names.

Try for yourself 'DottedDiamond', 'Shingle', or 'LargeConfetti', or even for the golfers like Ginny, the one called 'Divot' :-)

Having done nice things with hatching, let us move to applying our own 'texture' to the brush with a bitmap file. There are plenty to choose from on the web.

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

Try downloading a few small bitmap images yourself, and fill a rectangle with them. Then a polygon.

Okay, now we have to "push the boat out" and try the gradient brushes, 'Linear' and 'Path'.

You can use a linear gradient brush to fill a shape with colour that changes gradually as you move across the shape horizontally, vertically, or diagonally. The following code example shows how to fill a rectangle with a horizontal gradient brush that changes from blue to green as you move from the left edge of the rectangle to the right edge. The resulting output is shown alongside the code.

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

When it comes to the PathGradientBrush I should really say that it is outside the scope of the ClickStartVDN concept. The SDK is not easy to follow and sort out, and if you get that far you need a good book and will be able to help yourself..

However, just to show we can get some sort of an output see the small example below and the output it produces. Note also that "USING System.Drawing" and "USING System.Drawing.Drawing2D" is needed at the top of your PRG code to allow you to be able to simplify your code, and the Namespace is then just implied.

I suspect you could spend a long while getting to grips with PathGradientBrush as a tool, and may get some dreadful results before you get beautiful ones !

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

You will see that we seem to get a strange effect, but this is just a result of having our four points NOT in a sequence order, as when you go arond the rectangle in the normal fashion. Beware of the importance of the particular order of the points.

Shown below is the result after re-ordering the points and specifying an array of colors which has one for each point in the set.

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

Lines :-

To draw lines on our graphics surface is quite straight forward, but the variety of types and styles is quite vast ! It is the Pen object which determines the look of the line. We also need to consider what we wish our 'joined' lines to look like.

Lets take one thing at a time and draw a few lines on our test form to see what we have to work with.

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

As well as being able to control the position of the lines on the graphic surface we appear to be also in control of the colour and thickness of the lines.

This is fine for a start, now let us see what types of style and pattern we may have on request.

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

You can try the enumeration for yourself which gives all dots.

So if it is the pen object which determines the line style, then drawing shapes and curves with such pen specifications will produce what we might expect. Dotted circles, and dot dash rectangles, and the likes.

Finally, let us look at how to start and finish lines with 'caps' chosen from the enumeration called 'Linecap'.

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

Look out the rest of the 'Linecap' enumerations for yourself. Remember they are shown by intellisense so you don't need to root them out from the SDK.

Good luck with experimenting further with lines, and don't forget to try out the joining style feature for lines making shapes.

Pens :-

I drawing our lines above, we have already seen something of what can be done with pens. The pen object is quite complex in many ways, but is not difficult to use if you done some experimenting first.

It would be nice to be able to say that "lines are drawn using pens and shapes filled with brushes". Although true, it is more complex than it sounds, as the pen object has a 'brush' property. When this is set it over-rides the 'color' property and the pen behaves differently.

The SDK documentation simply says :- "Assigning this property causes the pen to draw filled lines and curves. It overrides the Color property of the Pen".

Let us put this to the test. It would appear that if we assign to our pen object a brush property, where the brush is of 'linear gradient' type we ought to get something like so :-

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

You will notice that using the overload for the brush which takes the start and end points for the gradient fill, means we get some very "odd" line ends. This can be easily put right when we realise that the line caps go beyond the end of the lines as specified. WE need to change the points for the brush so they move a little bit away from the ends of the line as specified.

I think this is an extension exercise which you ought to do yourself. I have given you enough pointers as to what is going on with pens and lines.

I am not sure what can be done with 'Path Gradient' brushes when filling lines with a pen of this type. Please do your homework and them email me via the VOPS and Pearls news groups :-)

Color(s) :-

In comparison to the classes used for graphics and the graphic surface, the color 'structure' is quite simple, or straight forward.

Yes, it is not a class, but a structure. It does have properties and methods however. (Refer to related sub-section "Color - Class V Structure")

As well as being able to specify a color by the RGB values making it up, there are a host of pre-defined, and suitably named colors to make life easy.

The SDK documentation says that color members represent an ARGB color.

Let us look at some examples to see how things are done. If we want our color object to be the system defined color "Cyan" we do :-

LOCAL oColor as Colour

oColor := Color.Cyan

There are a huge number to choose from and these are all presented in the intellisense list in the IDE so you don't need to go elsewhere.

However, an easy way to see these is to use the properties tab in VIDE and select the form or suitable control and try to change BackColor or ForeColor. The followings will be seen on screen :-

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

You will be interested to know that we could have made a "crimson" color object from its name as follows :-

LOCAL oColCrimson as Color

oColCrimson := Color.FromName("Crimson")

The simpler way is just :-

oColCrimson := Color.Crimson

And, in fact we can then split this up into the four values A,R,G, and B - which stand for Alpha, Red, Green, and Blue.

The RGB are as would be expected for colors (Red, Green, Blue) values from 0 to 255. The Alpha value is a similar type of value but represents how Transparent the resulting color will be when seen on the screen or page.

A value of 255 for A (alpha) is solid, and 0 totally transparent.

To see just what is going on let us take a real example and run it. Let us take SlateBlue as a color to see what values make it up.

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

You will see why we chose SlateBlue, it is because it has reasonably sized contributions from each of the three colors R,G & B.

Okay, we are starting to see what colors are all about, so if we can split a defined color down into its constituent parts, then we should also be able to make one up from a set of given values of the ARGB format.

By now you will have noticed that each of these four values is a BYTE, so they each can have a specific value from 0 through 255.

Now let us turn our attention to the "A" or alpha part of a color object.

Below is a real live screen output of a suitable example to study :-

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

This is part of the test print made by the Examine Printer applet which is sent to a duplex printer. Two pages are sent, the first one with a part transparent red "front" marked as a water mark type feature, and the second with a green "back" marked in a similar way.

The code for this red front is shown below :-

IF SELF:bDuplex

IF SELF:FirstPage

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)

Notice I have used 75 as a value for the alpha byte - this is the color's transparent constituent.

You will notice that to make a color from its four constituents (8-bit components) we use the FromArgb() method. There are four overloads for this method which you need to check out in the SDK. They allow you to be very flexible in your coding approach, not specifying the "A" component if you are not interested in transparency.

It is outside the scope of these notes to go any deeper into such matters as transparency, suffice it to say that whether the red "front" id printed first or last the output looks the same (as above) so there is something more going on with the than just printing order.

I leave it for you to follow this up for yourself if you wish to know and use more of this feature. Here is what the SDK has to say - under "Alpha Blending" !

"When you fill a shape, you must pass a Brush object to one of the fill methods of the Graphics class. The one parameter of the SolidBrush constructor is a Color object. To fill an opaque shape, set the alpha component of the color to 255. To fill a semitransparent shape, set the alpha component to any value from 1 through 254.

When you fill a semitransparent shape, the color of the shape is blended with the colors of the background. The alpha component specifies how the shape and background colors are mixed; alpha values near 0 place more weight on the background colors, and alpha values near 255 place more weight on the shape color."

You do need a book on GDI+ however, and you also need to look out the Color.Transparent property too.

Good luck with your transparent writing / printing :-)

Fonts :- { to see how to find the fonts & families available on the PC look at sub-section on Fonts}

In this sub-section it is sufficient to say that a font is made up from a font family and a size. So "Arial 16" is a font, Aril being the name of the font family.

If the font is to display as italic or bold, then these are 'styles' of the font. Other styles are underline, strike-through, and regular. Not all styles may be available for all font families.

The whole subject of font, and font families, is quite huge as well as complex, so we will just have to come to grips with enough to get by and do the basic sort of things required by printing text.

Before we can draw text, or string data, onto a Form we need to have a font instantiated.

Lets us now try to instantiate a few popular Windows fonts and place some text on a Form :-

Text :-

Text is drawn on the specified graphics surface in a similar manner to to lines and shapes. Before we can draw text (or string data) we need to have a font instantiated.

Here is a typical line of code :- ( this is directly from my BasicDocument class - method named 'LineTextPrint' )

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

You will notice that as well as the text itself, we need to supply the font and a brush, as well as the coordinates of where to start the string drawing on the graphics surface.

It may spring to you mind that if we use a brush object we can then tap into all the power and flexibility of that object - once we can get the basics to work. Here I am referring to filling the text letters with hatching, texture and also linear gradient type of effects.

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

Okay, this is fine, looks good and is quite easy to do. Lets try a different font family, with a different colour brush.

And, maybe apply some different styles to different lines of text as well. Lets see what we can do :-

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

Notice the syntax of the line which sets the font to bold. It would appear that for those of us used to turning styles on and off for a font, we have to learn new tricks !

Once a font is instantiated you may find out what the style is, but not change it without instantiating a different font. However, at least you may use the existing font in the overload.

At this point you ought to take a look into the SDK for "System.Drawing.Font", and see just what the properties and methods are - as well as the overloads for the font instantiation.

Before I leave it to you to experiment and have fun lets try some nice "fun" effects :-

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

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

Finally, we need to look at how text can be constrained to be in a specified rectangle on the graphics surface.

This can be useful in it's own rights, however, combined with brushes made from 'special' fills we can get some great looking effects.

Let us look at a specific example. First view the working code and resulting output shown below :-

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

First of all it looks complicated, but there is an easy way to look at what we have done.

Think of those greetings cards we have seen, where the letters have been cut out of the card to leave a hole. Then the designers of the card have placed in the next 'layer' a sheet of backing paper, often with a gold, silver, or possibly red metallic finish to it.

The overall effect is to colour the letters. If we now look at our output then this is what it looks like - a greetings card style transparent text effect.

Obviously, the area specified for the special brush fill, and the area where the text is constrained, needs to be 'overlapping' - just in the same way the backing paper on the greetings card needs to be placed behind the cut-out letters on the top layer of the card.

You may wish to try and use hatched and textured brush fills, as well as linear gradient and path gradient effects.

Even try a linear gradient brush with the gradient going from top to bottom - opposite corners.

I now think it is time to ask you to do some of your own trials to see what nice and interesting effects you can achieve.

Good Luck :-)

PHH Jan. 2008


Fonts - All about

Fonts

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 TimesNewRoman, 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.

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

So once we can produce a list of the font families we can then select a few to see if they are of regular style.

Below is a working example of a section of code to see which these are in the range 13 to 49 of my available set.

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

You could of course use another of the font styles from the enumeration - see the image below to see which are available from the intellisense option box.

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

Let us try 'Italic' on the family range 79 through 96 on my system :-

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

Now we are in a position to have some interesting fun.

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 Jan. 2008


Color - Class V Structure

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 Feb. 2008


Full Working Code

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

LOCAL oBrush AS SolidBrush

LOCAL g AS Graphics

g := e:Graphics

oBrush := SolidBrush{Color.Red}

g:FillEllipse(oBrush, 0,0,200,150)

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

oBrush := SolidBrush{Color.Yellow}

g:FillEllipse(oBrush, 60,40,75,75)

oBrush:Dispose()

g:Dispose()

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

LOCAL oBrush AS SolidBrush

LOCAL g AS Graphics

LOCAL oPen AS pen

g := e:Graphics

oPen := Pen{Color.Black,3}

g:DrawEllipse(oPen, 0,0,200,150)

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

oPen := Pen{Color.Blue,5}

g:DrawEllipse(oPen, 60,40,75,75)

oPen:Dispose()

g:Dispose()

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

LOCAL oSBBW AS Brush

LOCAL oRect AS Rectangle

LOCAL g AS Graphics

oSBBW := SolidBrush{Color.BurlyWood}

oRect := Rectangle{13,13,200,200}

g := e:Graphics

g:FillRectangle(oSBBW, oRect)

oSBBW:Dispose()

g:Dispose()

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

LOCAL oBrush AS HatchBrush

LOCAL g AS Graphics

g := e:Graphics

oBrush := HatchBrush{HatchStyle.Cross, Color.Blue, Color.DarkKhaki}

g:FillEllipse(oBrush, 0,0,200,150)

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

oBrush := HatchBrush{HatchStyle.DiagonalBrick,Color.Yellow, Color.Red}

g:FillEllipse(oBrush, 60,40,75,75)

oBrush:Dispose()

g:Dispose()

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

LOCAL oBrush AS TextureBrush

LOCAL g AS Graphics

LOCAL oImage AS Image

g := e:Graphics

oImage := Image.FromFile("C:\VDNUK\FlashMove.jpg")

oBrush := TextureBrush{oImage}

g:FillEllipse(oBrush, 0,0,200,150)

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

oImage := Image.FromFile("C:\VDNUK\Triang.jpg")

oBrush := TextureBrush{oImage}

g:FillEllipse(oBrush, 30,150,200,150)

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

oImage := Image.FromFile("C:\VDNUK\Delta.jpg")

oBrush := TextureBrush{oImage}

g:FillEllipse(oBrush, 200,0,130,90)

oBrush:Dispose()

g:Dispose()

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

LOCAL g AS Graphics

LOCAL oLGB AS LinearGradientBrush

LOCAL oRect AS Rectangle

oRect := Rectangle{13,13,200,200}

oLGB := LinearGradientBrush{oRect, Color.Blue, Color.Green, LinearGradientMode.Horizontal}

g := e:Graphics

g:FillRectangle(oLGB, oRect)

oLGB:Dispose()

g:Dispose()

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

//---- USING System.Drawing - else needs to be in code ! ----

//---- System.Drawing.Drawing2D.PathGradientBrush{........} ----

//---- USING System.Drawing.Drawing2D - needed for Path/Linear GradientBrush brush ----

LOCAL g AS Graphics

LOCAL oPGB AS PathGradientBrush

LOCAL oRect AS Rectangle

LOCAL PGpoints AS Point[]

LOCAL aColors AS Color[]

PGpoints := Point[]{4}

PGpoints[1] := Point{13,13}

PGpoints[2] := Point{13,200}

PGpoints[3] := Point{200,200}

PGpoints[4] := Point{200,13}

aColors := Color[]{4}

aColors[1] := Color.Red

aColors[2] := Color.Yellow

aColors[3] := Color.Green

aColors[4] := Color.Blue

oRect := Rectangle{13,13,200,200}

oPGB := PathGradientBrush{PGpoints}

oPGB:CenterColor := Color.Cyan

oPGB:SurroundColors := aColors

g := e:Graphics

g:FillRectangle(oPGB, oRect)

oPGB:Dispose()

g:Dispose()

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

LOCAL g AS Graphics

LOCAL oPen AS Pen

LOCAL oPoint1, oPoint2 AS Point

g := e:Graphics

oPen := Pen{Color.Black}

oPoint1 := Point{13,13}

oPoint2 := Point{200,13}

g:DrawLine(oPen, oPoint1, oPoint2)

oPen := Pen{Color.Black,3}

oPoint1 := Point{13,63}

oPoint2 := Point{200,63}

g:DrawLine(oPen, oPoint1, oPoint2)

oPen := Pen{Color.Green,6}

oPoint1 := Point{13,113}

oPoint2 := Point{200,113}

g:DrawLine(oPen, oPoint1, oPoint2)

oPen:Dispose()

g:Dispose()

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

//oPen:DashStyle := DashStyle.Dash

//oPen:DashStyle := DashStyle.DashDot

//oPen:DashStyle := DashStyle.DashDotDot

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

OCAL g AS Graphics

LOCAL oPen AS Pen

LOCAL oPoint1, oPoint2 AS Point

LOCAL oLGB AS LinearGradientBrush

g := e:Graphics

oPen := Pen{Color.BlueViolet,10}

oPen:EndCap := Linecap.ArrowAnchor

oPen:StartCap := Linecap.ArrowAnchor

oPoint1 := Point{13,13}

oPoint2 := Point{200,13}

oLGB := LinearGradientBrush{oPoint1, oPoint2, Color.Blue, Color.Green}

oPen:Brush := oLGB

g:DrawLine(oPen, oPoint1, oPoint2)

oPen:EndCap := Linecap.Triangle

oPen:StartCap := Linecap.Triangle

oPoint1 := Point{13,63}

oPoint2 := Point{200,63}

oLGB := LinearGradientBrush{oPoint1, oPoint2, Color.Green, Color.Yellow}

oPen:Brush := oLGB

g:DrawLine(oPen, oPoint1, oPoint2)

oPen:EndCap := Linecap.DiamondAnchor

oPen:StartCap := Linecap.DiamondAnchor

oPoint1 := Point{13,113}

oPoint2 := Point{200,113}

oLGB := LinearGradientBrush{oPoint1, oPoint2, Color.Yellow, Color.Red}

oPen:Brush := oLGB

g:DrawLine(oPen, oPoint1, oPoint2)

oLGB:Dispose()

oPen:Dispose()

g:Dispose()

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

OCAL cText AS STRING

LOCAL oBrush AS Brush

LOCAL g AS Graphics

LOCAL oFont AS Font

LOCAL oPoint1, oPoint2 AS Point

LOCAL oLGBrush AS LinearGradientBrush

oPoint1 := Point{13,113}

oPoint2 := Point{600,113}

oLGBrush := LinearGradientBrush{oPoint1, oPoint2, Color.Red, Color.Blue}

oBrush := SolidBrush{Color.BurlyWood}

oFont := Font{"Century Schoolbook", 26}

oFont := Font{oFont,FontStyle.Italic}

g := e:Graphics

cText := "Hello - Pioneers of Vulcan !"

g:DrawString(cText, oFont, oBrush, 13, 13)

oFont := Font{oFont,FontStyle.Underline}

oBrush := SolidBrush{Color.CornflowerBlue}

cText := "We are Vulcaneers !!"

g:DrawString(cText, oFont, oBrush, 13, 63)

oFont := Font{oFont,FontStyle.Strikeout}

cText := "Sea our Beautiful syntax !!!"

g:DrawString(cText, oFont, oLGBrush, 13, 113)

oBrush:Dispose()

oFont:Dispose()

g:Dispose()

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

OCAL cText AS STRING

LOCAL oTBrush AS TextureBrush

LOCAL g AS Graphics

LOCAL oFont AS Font

LOCAL oImage AS Image

cText := "Light?"

oFont := Font{"Goudy Stout", 120}

g := e:Graphics

oImage := Image.FromFile("C:\VDNUK\Delta.jpg")

oTBrush := TextureBrush{oImage}

oFont := Font{oFont,FontStyle.Bold}

g:DrawString(cText, oFont, oTBrush, 0, 0)

oTBrush:Dispose()

oFont:Dispose()

g:Dispose()

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

PROTECTED METHOD OnPaint(e AS PaintEventArgs) AS VOID

//---- USING System.Drawing - else needs to be in code ! ----

//---- Using System.Drawing.Drawing2D ----

LOCAL cText AS STRING

LOCAL oPGBrush AS PathGradientBrush

LOCAL g AS Graphics

LOCAL oFont AS Font

LOCAL oRect AS RectangleF

LOCAL PGpoints AS Point[]

LOCAL aColors AS Color[]

cText := "Have you seen the light at the end of the tunnel? I think it's getting brighter just a touch!"

oFont := Font{"Goudy Stout", 9}

g := e:Graphics

PGpoints := Point[]{4}

PGpoints[1] := Point{0,0}

PGpoints[2] := Point{0,200}

PGpoints[3] := Point{200,200}

PGpoints[4] := Point{200,0}

aColors := Color[]{4}

aColors[1] := Color.Red

aColors[2] := Color.Yellow

aColors[3] := Color.Green

aColors[4] := Color.Blue

oRect := Rectangle{13,13,190,190}

oPGBrush := PathGradientBrush{PGpoints}

oPGBrush:CenterColor := Color.Cyan

oPGBrush:SurroundColors := aColors

g:DrawString(cText, oFont, oPGBrush, oRect)

oPGBrush:Dispose()

oFont:Dispose()

g:Dispose()

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

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

LOCAL oCol AS Color

LOCAL a,r,g,b AS BYTE

LOCAL cText, cHead AS STRING

oCol := Color.FromName("SlateBlue")

a := oCol:A

r := oCol:R

g := oCol:G

b := oCol:B

cHead := "Anatomy of a Color :"

cText := "SlateBlue is made up as :-"+CRLF

cText += a:tostring():PadRight(11)+" : Alpha"+CRLF

cText += r:tostring():PadRight(11)+" : Red"+CRLF

cText += g:tostring():PadRight(11)+" : Green"+CRLF

cText += b:tostring():PadRight(11)+" : Blue"+CRLF

MessageBox.show(cText, cHead)

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

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

METHOD GetFamilies() AS VOID

//--------- System.Drawing.FontFamily class ----------

LOCAL g AS Graphics

LOCAL aFFams AS FontFamily[]

LOCAL cText, cHead AS STRING

g := SELF:CreateGraphics()

aFFams := FontFamily.GetFamilies(g);

cHead := "Font Families : "

cText := "Total number :- "+aFFams:Length:ToString()+CRLF+CRLF

//cText += "FF 131 :- "+aFFams[131]:ToString()+CRLF

cText += "FF 129 :- "+aFFams[129]:Name+CRLF

cText += "FF 130 :- "+aFFams[130]:Name+CRLF

cText += "FF 131 :- "+aFFams[131]:Name+CRLF

cText += "FF 132 :- "+aFFams[132]:Name+CRLF

cText += "FF 133 :- "+aFFams[133]:Name+CRLF

cText += "FF 134 :- "+aFFams[134]:Name+CRLF

MessageBox.Show(cText, cHead)

RETURN

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

PROTECTED METHOD OnPaint(e AS PaintEventArgs) AS VOID

// ---- USING System.Drawing - else needs to be in code ! ----

// ---- Using System.Drawing.Drawing2D ----

LOCAL i, nSize, nCount AS DWORD

LOCAL cText1, cText2, cText3, cText4, cName AS STRING

LOCAL oFont AS Font

LOCAL oPoint AS Point

LOCAL oSBrush AS SolidBrush

LOCAL g AS Graphics

LOCAL aFFams AS FontFamily[]

g := e:Graphics

aFFams := FontFamily.GetFamilies(g)

nSize := 12

nCount := 0

FOR i := 130 UPTO 139

cName :=aFFams[i]:Name

oFont := Font{cName, nSize}

cText1 := cName

oPoint := Point{10,10+nCount*17}

oSBrush := SolidBrush{Color.Blue}

g:DrawString(cText1, oFont, oSBrush, oPoint)

nCount +=1

NEXT

nCount := 0

FOR i := 170 UPTO 179

cName :=aFFams[i]:Name

oFont := Font{cName, nSize}

cText1 := cName

oPoint := Point{310,10+nCount*17}

oSBrush := SolidBrush{Color.Red}

g:DrawString(cText1, oFont, oSBrush, oPoint)

nCount +=1

NEXT

nCount := 0

FOR i := 190 UPTO 199

cName :=aFFams[i]:Name

oFont := Font{cName, nSize}

cText1 := cName

oPoint := Point{610,10+nCount*17}

oSBrush := SolidBrush{Color.Green}

g:DrawString(cText1, oFont, oSBrush, oPoint)

nCount +=1

NEXT

oSBrush:Dispose()

oFont:Dispose()

g:Dispose()

RETURN

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


Section 34 - Drawing Graphs

Enter topic text here.


ZedGraph - third party

Paul Piko kindly pointed me in the direction of the third party product "ZedGraph".

He gave me the following links which have led to success, as you will see below.

>> How about ZedGraph?: http://zedgraph.org/wiki/index.php?title=Main_Page
>> Tutorial here: http://www.codeproject.com/KB/graphics/zedgraph.aspx

I downloaded the DLL and went to the tutorial link to get the C# version of the code - now converted into Vulcan syntax and shown in the following sub-section "Full Working Code".

The instructions given by the Code Project Team who were responsible for the creation of ZedGraph, are angled at Visual Studio as they are listed in the getting going link.

However, Chris Pyrgas is to make VIDE work in a similar way in future, and you will be then able to repeat what I have done below.

For now, it is sufficient for you to know that he kindly supplied me with a special "Config" file which provided a ZedGraph tab in the Toolbox with a ZedGraph control which could be dragged onto a new and blank Form.

Paul is supplying a VS form of these examples and this code will be available through the "goVulcan" site.

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

This output is the one produced by CreateGraph() as opposed to the CreateGraph2() method.

You may also like to know that this method is in fact the combination of the first and second parts of the tutorial examples - code sections.

Please also notice that the code for the "Interesting Point" is incorrect in the tutorial and that the coordinates needed to be re-adjusted so that the text and arrow could be displayed on the graph area shown.

This anomaly was spotted by Alwyn (AJB) - thanks Alwyn, pleased to know someone is looking at what I am doing with my weekends !

METHOD BasicFormLoad(o AS OBJECT , e AS EventArgs) AS VOID

// Setup the graph

SELF:CreateGraph( ozedGraphControl1 )

//SELF:CreateGraph2( ozedGraphControl1 )

// Size the control to fill the form with a margin

SELF:SetSize()

RETURN

In the code section above (included in the full working code), switch between the create methods to achieve different results.

When you do this and use method two you should then get what is shown below :-

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

It is not the focus of ClickStartVDN to go further than this so all I will do is say that you need to look out the possible methods and properties of the class and read the supporting tutorial. There are other sample working app too, but they will probably be in C# syntax.

If all goes well in the future I may do a ClickGraphVDN in which deeper and fuller exploration of graphing is explored.

Good graphing !

Don't forget to comment out one of the create graph methods or you get a strange mix of the two outputs when both methods are being used!

PHH Feb. 2008


Full Working Code

USING System.Windows.Forms

USING System.Drawing

USING ZedGraph

CLASS BasicForm INHERIT Form

PROTECT ozedGraphControl1 AS ZedGraph.ZedGraphControl

PROTECT oOKButton AS Button

// User code starts here (DO NOT remove this line) ##USER##

CONSTRUCTOR()

SUPER()

SELF:InitializeForm()

SELF:oOKButton:Hide()

RETURN

METHOD InitializeForm() AS VOID

// IDE generated code (please DO NOT modify)

SELF:Name := "BasicForm"

SELF:SuspendLayout()

SELF:Location := Point{100,100}

SELF:ClientSize := Size{650,510}

SELF:Text := "Basic form application"

SELF:Load += EventHandler{ SELF , @BasicFormLoad() }

SELF:Resize += EventHandler{ SELF , @BasicFormResize() }

SELF:ozedGraphControl1 := ZedGraph.ZedGraphControl{}

SELF:ozedGraphControl1:Name := "zedGraphControl1"

SELF:ozedGraphControl1:Location := Point{ 8 , 8 }

SELF:ozedGraphControl1:Size := Size{ 632 , 416 }

SELF:ozedGraphControl1:TabIndex := 1

SELF:Controls:Add(SELF:ozedGraphControl1)

SELF:oOKButton := Button{}

SELF:oOKButton:Name := "OKButton"

SELF:oOKButton:Location := Point{ 568 , 480 }

SELF:oOKButton:Size := Size{ 80 , 24 }

SELF:oOKButton:Text := "&OK"

SELF:oOKButton:Click += EventHandler{ SELF , @OKButtonClick() }

SELF:oOKButton:TabIndex := 0

SELF:Controls:Add(SELF:oOKButton)

SELF:ResumeLayout()

RETURN

METHOD BasicFormResize(o AS OBJECT , e AS EventArgs) AS VOID

SELF:SetSize()

RETURN

METHOD SetSize() AS VOID

ozedGraphControl1:Location := Point{ 10, 10 }

// Leave a small margin around the outside of the control

ozedGraphControl1:Size := Size{ ClientRectangle:Width - 20, ClientRectangle:Height - 20 }

RETURN

METHOD BasicFormLoad(o AS OBJECT , e AS EventArgs) AS VOID

// Setup the graph

SELF:CreateGraph2( ozedGraphControl1 )

// Size the control to fill the form with a margin

SELF:SetSize()

RETURN

METHOD CreateGraph(zgc AS ZedGraphControl ) AS VOID

//--------- Thanks to Chris Pyrgas --- VERY MUCH !!! ---------

//--------- for making this happen ---------------------------

// get a reference to the GraphPane

LOCAL myPane AS GraphPane

myPane := zgc:GraphPane

// Set the Titles

myPane:Title:Text := "MANY Thanks to Chris Pyrgas & Paul Piko :"

//myPane:Title:Text := "My Test Graph\n(For CodeProject Sample)"

myPane:XAxis:Title:Text := "My X Axis"

myPane:YAxis:Title:Text := "My Y Axis"

// Make up some data arrays based on the Sine function

LOCAL x, y1, y2 AS Double

LOCAL list1, list2 AS PointPairList

LOCAL i AS DWORD

list1 := PointPairList{}

list2 := PointPairList{}

FOR i := 1 UPTO 35

x := (double)i + 5

y1 := 1.5 + Math.Sin( (double)i * 0.2 )

y2 := 3.0 * ( 1.5 + Math.Sin( (double)i * 0.2 ) )

list1:Add( x, y1 )

list2:Add( x, y2 )

NEXT

// Generate a red curve with diamond

// symbols, and "Porsche" in the legend

LOCAL myCurve AS LineItem

myCurve := myPane:AddCurve( "Porsche", list1, Color.Red, SymbolType.Diamond )

// Generate a blue curve with circle

// symbols, and "Piper" in the legend

LOCAL myCurve2 AS LineItem

myCurve2 := myPane:AddCurve( "Piper", list2, Color.Blue, SymbolType.Circle )

// Tell ZedGraph to refigure the

// axes since the data have changed

zgc:AxisChange()

//======================= next bit of example ================================

//SELF:NextBit2()

myPane:Title:FontSpec:FontColor := Color.Green

// Add gridlines to the plot, and make them gray

myPane:XAxis:MajorGrid:IsVisible := TRUE

myPane:YAxis:MajorGrid:IsVisible := TRUE

myPane:XAxis:MajorGrid:Color := Color.LightGray

myPane:YAxis:MajorGrid:Color := Color.LightGray

// Move the legend location

myPane:Legend:Position := ZedGraph.LegendPos.Bottom

//Make both curves thicker

myCurve:Line:Width := 2.0

myCurve2:Line:Width := 2.0

// Fill the area under the curves

myCurve:Line:Fill := Fill{ Color.White, Color.Red, 45.0 }

myCurve2:Line:Fill := Fill{ Color.White, Color.Blue, 45.0 }

// Increase the symbol sizes, and fill them with solid white

myCurve:@@Symbol:Size := 8.0

myCurve2:@@Symbol:Size := 8.0

myCurve:@@Symbol:Fill := Fill{ Color.White }

myCurve2:@@Symbol:Fill := Fill{ Color.White }

// Add a background gradient fill to the axis frame

myPane:Chart:Fill := Fill{ Color.White, Color.FromArgb( 255, 255, 210 ), -45.0 }

// Add a caption and an arrow

LOCAL myText AS TextObj

myText := TextObj{ e"\"Interesting\"\nPoint", 45.0,7.85}

myText:FontSpec:FontColor := Color.Red

myText:Location:AlignH := AlignH.Center

myText:Location:AlignV := AlignV.Top

myPane:GraphObjList:Add( myText )

LOCAL myArrow AS ArrowObj

myArrow := ArrowObj{ Color.Red, 12.0, 43.0, 7.0, 40.0, 6.6 }

myPane:GraphObjList:Add( myArrow )

RETURN

METHOD CreateGraph2(zgc AS ZedGraphControl ) AS VOID

// get a reference to the GraphPane

LOCAL myPane AS GraphPane

myPane := zgc:GraphPane

// Set the Titles

myPane:Title:Text := e"My \"Fancy\" Test Bar Graph"

myPane:XAxis:Title:Text := "Label"

myPane:YAxis:Title:Text := "My Y Axis"

// Make up some random data points

LOCAL y,y2,y3,y4 AS Double[]

LOCAL labels AS STRING[]

labels := STRING[]{6}

//labels := { "Panther", "Lion", "Cheetah", "Cougar", "Tiger", "Leopard" }

labels[1] := "Panther"

labels[2] := "Lion"

labels[3] := "Cheetah"

labels[4] := "Cougar"

labels[5] := "Tiger"

labels[6] := "Leopard"

//y := { 100, 115, 75, 22, 98, 40 }

y := Double[]{6}

y[1] := 100

y[2] := 115

y[3] := 75

y[4] := 22

y[5] := 98

y[6] := 40

y2 := Double[]{6}

y2[1] := 90

y2[2] := 100

y2[3] := 95

y2[4] := 35

y2[5] := 80

y2[6] := 35

y3 := Double[]{6}

y3[1] := 80

y3[2] := 110

y3[3] := 65

y3[4] := 15

y3[5] := 54

y3[6] := 67

y4 := Double[]{6}

y4[1] := 120

y4[2] := 125

y4[3] := 100

y4[4] := 40

y4[5] := 105

y4[6] := 75

// Generate a red bar with "Curve 1" in the legend

LOCAL myBar AS BarItem

myBar := myPane:AddBar( "Curve 1", NULL, y, Color.Red )

myBar:Bar:Fill := Fill{ Color.Red, Color.White, Color.Red }

// Generate a blue bar with "Curve 2" in the legend

myBar := myPane:AddBar( "Curve 2", NULL, y2, Color.Blue )

myBar:Bar:Fill := Fill{ Color.Blue, Color.White, Color.Blue }

// Generate a green bar with "Curve 3" in the legend

myBar := myPane:AddBar( "Curve 3", NULL, y3, Color.Green )

myBar:Bar:Fill := Fill{ Color.Green, Color.White, Color.Green }

// Generate a black line with "Curve 4" in the legend

LOCAL myCurve AS LineItem

myCurve := myPane:AddCurve( "Curve 4", NULL, y4, Color.Black, SymbolType.Circle )

myCurve:Line:Fill := Fill{ Color.White, Color.LightSkyBlue, -45.0 }

// Fix up the curve attributes a little

myCurve:@@Symbol:Size := 8.0

myCurve:@@Symbol:Fill := Fill{ Color.White }

myCurve:Line:Width := 2.0

// Draw the X tics between the labels instead of

// at the labels

myPane:XAxis:MajorTic:IsBetweenLabels := TRUE

// Set the XAxis labels

myPane:XAxis:Scale:TextLabels := labels

// Set the XAxis to Text type

myPane:XAxis:Type := AxisType.Text

// Fill the Axis and Pane backgrounds

myPane:Chart:Fill := Fill{ Color.White, Color.FromArgb( 255, 255, 166), 90.0 }

myPane:Fill := Fill{ Color.FromArgb( 250, 250, 255) }

// Tell ZedGraph to refigure the

// axes since the data have changed

zgc:AxisChange()

RETURN

METHOD NextBit2() AS VOID

// Change the color of the title

//myPane:Title:FontSpec:FontColor := Color.Green

RETURN

METHOD OKButtonClick(oControl AS OBJECT , e AS System.EventArgs) AS VOID

SELF:Close()

RETURN

END CLASS


Section 35 - Numbers and Maths

Exploring the 'System available' functions : (dotNet speak - Static Methods)

For those of you who need to use and manipulate numbers a great deal, then this is the section for you.

You may be from the scientific community like me, living in a world of equations and computer models, or the financial sector where money matters.

Obviously, engineers too also inhabit the world of equations and computer modelling, so numbers matter a great deal to them also.

This will just be sufficient coverage of mathematical functions and numeric manipulation to ease you into the dotNet way of doing things. Anything more, and you need to refer to other texts.

For most of us dealing with the 'day-to-day' world it is probably enough to know where to find the required math functions, and a little on how to use them. Once we get going we can sort ourselves out.

But for those of us who may need to worry about accuracy, and errors introduced due to complex calculations, where rounding errors are NOT acceptable, the you may need to look elsewhere.

The first thing I learned, many decades ago, when faced with computer programming for the first time, was understand how your numbers are represented inside the computer.

Once you know something about number representation, you then need to know about manipulation, and what problems this can lead to. In those days mixing integers and reals was a nightmare !

Making a Start :-

Enough of the chat, lets make a start and do something - write some code and test it.

Lets start at the mathematical functions end of things. The System.Math namespace is where we need to look.

We don't need a #USING statement as the System namespace is always available to us, we will just use the syntax 'Math.xxxx' where xxxx represents a method name.

The System.Math class is basically a set of static methods, and a couple of fields. Lets take the example of finding the value of PI from a public field, a bit like a static property.

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

Then we have a second well known constant little 'e', the base of natural logs. Will will also use the Ceiling static method to save on space, by doing two new functions at the same time.

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

Ceiling gives the smallest integer greater than the specified value. Try using Math.Floor() for yourself.

In this case floor gives a value of 2 as opposed to the value of 3 for the ceiling.

However, if we try to round we have to go a little further :-

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

In order to get the value supplied to four decimal places we have to supply a 4 as the second parameter in the method call.

You can look into how to specify what to do it the final digit in the precision required is mid way between. View the SDK documentation.

Okay, now we can do something a bit useful and find the volume of a sphere of specified radius. This equation needs one extra function not so far seen in action, and that is the power function. Here of course it is called a static method.

Lets have a look at this this equation in action - said in words as school boy speak, it is "four thirds pie are cubed" :-

So to find the volume of a sphere with a radius of two we have the following :-

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

In order to see the Power and Square Root functions in action look at a following sub-section called "Numbers in Maths".

Since most of these are pretty straight forward, both to use and understand, I will leave it to you to test out some of the others.

Try the trigonometrical static methods for sin, cos, tan and related functions. Also Min, Max, Trucate and IEEERemainder come in handy too at times.

Look in section 34 for the Math.Sin static method in action with ZedGraph output.

Good Luck with your fun testing.

PHH Feb. 2008


Numeric Types

The whole subject of "numbers" held in computers, and what happens when they are manipulated is rather complex.

Numbers are not just numbers, but a whole set of different types really. It all depends on what you wish to do and how accurately you wish to work, at the same time not taking too much time in the bargain :-)

As I said, not an easy topic really !

The problem gets worse when we realise that the numeric "types" can vary from one machine and operating system to another.

If you are old enough you have probably come through many variations in your time, from main frame language types at college and university, to early BASIC implementations on personal computers.

In other words, you've probably seen the lot, the good, the bad and the shear ugly ! I even remember Intel putting a chip on the market which had a flaw in in its numerical manipulation process, under certain specialised circumstances.

However, all is not lost, as one of the benefits of moving to dotNet is that the data type will remain the same, no matter if you are using C#, ASP, Vulcan or whatever.

Or in MS speak :-

To facilitate interoperability between languages, the .NET Framework types are CLS-compliant and can therefore be used from any programming language whose compiler conforms to the common language specification (CLS).

Also if dotNet is on a Unix box, then this constant set of data types is available there too. (That is also what the blue statement means !)

This is great news for folk who wish to write say in Vulcan code, but also use C# libraries which already exist. In the past this area was fraught with problems, and serious errors could occur by not understanding fully the data type requirements of the third party tool(s) being used.

Some more good news is that if you are not requiring to do any serious "number crunching" then all you need do is find out a basic set of "simplified" principles so that you do not create problems for yourself.

The old phrase "err on the right side" springs to mind.

Putting all this into a nut shell - stick to the dotNet data types and VOers keep clear of anything which is a hang over from earlier times !

In this section we are focusing on the 'Numeric' data types. What they are and when to use them to stay away from causing yourself trouble.

For most general use the sub-set of types we need to look at are :-

Int32

UInt32

Double

Decimal

Anyone wishing to do more than "general numeric work" should consult more suitable texts on the subject. Also I would suggest that everyone should consult and read the SDK documentation on Framework 2.0 and higher.

If you wish to see how trying to make this simpler is NOT EASY - try reading the first few lines of the Remarks about the "Double Structure" in the SDK.

But the "Catch 22" situation is that we all need to use numbers, and numeric types at some stage. So how much can you get away without knowing ?

On the subject of integers, only use them when you really have integers. Mixing integer variables with floating point variables in calculations can cause "strange" results to the unwary and ill informed. We have all fallen foul to this over the years. And, judging by recent News Group messages we still are - well at least on the amount of accuracy we are carrying.

I suggest also that everyone consults the Vulcan.NET help notes on Data Types in VS - part of it looks like this :-

For most of us, for most things Double is fine to use for numbers not needing to be integers.

For most integers, needing possibly to be negative at some point, then INT (System.Int32) is fine.

For variables which should never be less than zero then DWORD is fine.

Here is a bit more MS wisdom :-)

Double :

Double precision values with double type have 8 bytes. The format is similar to the float format except that it has an 11-bit excess-1023 exponent and a 52-bit mantissa, plus the implied high-order 1 bit. This format gives a range of approximately 1.7E–308 to 1.7E+308 for type double.

Microsoft Specific :

The double type contains 64 bits: 1 for sign, 11 for the exponent, and 52 for the mantissa. Its range is +/–1.7E308 with at least 15 digits of precision.

Int32 :

The Int32 value type represents signed integers with values ranging from negative 2,147,483,648 through positive 2,147,483,647.

Int32 provides methods to compare instances of this type, convert the value of an instance to its String representation, and convert the String representation of a number to an instance of this type.

For information about how format specification codes control the String representation of value types, see Formatting Overview.

DWORD :

The UInt32 value type represents unsigned integers with values ranging from 0 to 4,294,967,295.

UInt32 provides methods to compare instances of this type, convert the value of an instance to its String representation, and convert the String representation of a number to an instance of this type.

Decimal : ( Keep well away from this data type unless you really need the precision.)

If you are worried about rounding errors then look into the System.Decimal structure. Even when you know what you are after it is hard to find stuff to read which makes good sense to the "standard guy" I'm afraid.

No doubt some better explanations will arrive in time. For now, Good Luck !

Here's one Alwyn found for us :-

The Decimal type was introduced to reduce rounding errors in complex calculations when dealing with financial matters. It is a 128 bit value though not all bits are used. It allows you to use any number with up to 28 decimal digits (as against 15 for the Double) with the decimal point placed before the first digit, after the last digit, or anywhere between - a truly huge precision. However, the number of bits used for the exponent is only 5, compared with 11 for the Double, so the actual numeric value range is much smaller than the Double.

This makes it better for accuracy with real world monetary values but less capable of quoting astronomical distances in kilometres. It is only in complex calculations, where noticeable rounding errors might otherwise creep in, that this precision is required.

However, beware of how you use it since, for example, multiplying it by a Double will required type casting. Keep well away from this data type unless you really need the precision. The name "Decimal" seems beguilingly simple.

Well found Alwyn ;-)

P.S. He got the above from http://www.yoda.arachsys.com/csharp/decimal.html and the MSDN.

And here's a bit from Chris too - this also makes things a bit clearer as to what its all about !

In short, the "problem" with REALS, is that they internally store numbers in binary format. For example:

2 (decimal) = 10 in binary
1 = 1
0.5 = 0.1
0.25 = 0.01
0.75 = 0.11
1.25 = 1.01

etc..The above decimal numbers can be exactly represented in binary, so there's no lost of precision. But numbers like 0.6, 0.7 etc cannot be represented in binary without precision lost, same as the number 1 / 3 (=0.33333..) can't be represented exactly in the decimal system. So there's a precision lost when you add/subtract etc such numbers. In the decimal sample you'd get:

0.3333... + 0.3333... + 0.3333... = 0.9999.. which is not equal to 1.0000

Same thing happens with REALS. The decimal datatype doesn't use a binary format internaly, it stores numbers in decimal (the way a human would store it), so there's no precision lost. Hope I'm making some sense ;-)

Thanks for that too.

PHH Feb. 2008


Numbers in Maths

Let us now use the System.Math static methods to do some real problem solving, and enjoy a bit of maths.

First the quadratic equation :

Let us see how we can solve this equation using Vulcan syntax and static methods of the dotNet System.Math namespace :-

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

I will leave it up to you to put in the code that will first check to see that there are 'real' roots - or that b squared is not less than 4ac.

If the check is not in then it is possible to get a runtime error. Try the above code but with the value of c as 4 rather than minus 4 and see what happens.

And now to take this a final step forward we need to round our answer for :-

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

Summing a Series :

Fine we are doing Okay, but now lets branch out a bit and see if we can calculate for ourselves a mathematical function which we can compare with that return directly from the static method also used to generate the same value.

Let us use a series to calculate the value of 'log x' and use a test value of 2 to see if it works as it should.

Study the code below and the live output :-

You will notice that we are still using a static method to calculate each term, so we could go further and substitute our own code for that too.

Also notice that the limit of the iteration was with n equal to 29. This was not arbitrary, it was so the values became exactly equal to the last decimal place shown above.

We will discuss more about this later. For now lets just go that bit further to replace the Math.Pow() method.

We still need to go to 15 terms in the series to make the two values the same to that accuracy shown, but we are using no other mathematical static methods to help us do the calculation.

If you wish to have a bit of fun with this approach, then change the code so that instead of going a fixed number of iterative steps, you specify an accuracy which is compared to the next term to be added.

When the term is small enough (less than the accuracy provided as a parameter) then the iteration stops.

Finally notice the choice of the numeric data type used. Double precision is most appropriate, but see numeric types in the previous sub-section.

It would appear that the choice was quite suitable, but if your code was to do a great number of calculations of this complexity and greater, then you would need to try and do some optimising to get best performance.

We are now getting to a point where you need to consult other material to help you move into such computation and calculation.

PHH Feb.2008


FullWorking Code

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

/*

LOCAL x1, x2, a, b, c AS Double

LOCAL sr AS Double

LOCAL cText AS STRING

a := 2.0

b := -4.0

c := -3.0

sr := Math.Sqrt(b*b-4.0*a*c)

x1 := Math.Round((-b+sr)/(2.0*a), 2)

x2 := Math.Round((-b-sr)/(2.0*a), 2)

cText := "Solution to Quadratic Equation : "+CRLF+CRLF

cText += "Root one :- "+x1:ToString()+CRLF

cText += "Root two :- "+x2:ToString()+CRLF

MessageBox.Show(cText,"System.Math class :-")

*/

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

/* LOCAL x1, x2, a, b, c AS Double

LOCAL sr AS Double

LOCAL cText AS STRING

a := 1.0

b := 3.0

c := -4.0

sr := Math.Sqrt(b*b-4.0*a*c)

x1 := (-b+sr)/(2.0*a)

x2 := (-b-sr)/(2.0*a)

cText := "Solution to Quadratic Equation : "+CRLF+CRLF

cText += "Root one :- "+x1:ToString()+CRLF

cText += "Root two :- "+x2:ToString()+CRLF

MessageBox.Show(cText,"System.Math class :-")

*/

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

/* LOCAL nVolume, r, nRound AS Double

LOCAL cVolume, cText, cRound AS STRING

r := 2.0

nVolume := (4.0 * Math.PI * Math.Pow(r,3) )/3.0

nRound := Math.Round(nVolume,4)

cVolume := nVolume:ToString()

cRound := nRound:ToString()

cText := "Voulume of Sphere to 4 plcs. :- "+cRound+CRLF

MessageBox.Show(cText,"System.Math class :-")

*/

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

/* LOCAL nOrigVal, nValue AS Double

LOCAL cValue, cOrigVal, cText AS STRING

nOrigVal := Math.PI

cOrigVal := nOrigVal:ToString()

nValue := Math.Round(nOrigVal,4)

cValue := nValue:ToString()

cText := "Original Value of Pi :- "+cOrigVal +CRLF

cText += "Round Pi to 4 places :- "+cValue +CRLF

MessageBox.Show(cText,"System.Math class :-")*/

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

/* LOCAL nOrigVal, nValue AS Double

LOCAL cValue, cOrigVal, cText AS STRING

nOrigVal := Math.E //------- called little 'e' by school boys -------

cOrigVal := nOrigVal:ToString()

nValue := Math.Ceiling(nOrigVal)

cValue := nValue:ToString()

cText := "Value of 'e' :-"+cOrigVal +CRLF

cText += "Ceiling of 'e' :-"+cValue +CRLF

MessageBox.Show(cText,"System.Math class :-") */

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

/*

LOCAL nValue AS Double

LOCAL cValue AS STRING

nValue := Math.PI

cValue := nValue:ToString()

MessageBox.Show("Value of PI is :- "+cValue,"System.Math class :-")*/

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

LOCAL Y,YS, Sum AS Double

LOCAL x, T, LogX, Tn, NewT AS Double

LOCAL n AS DWORD

LOCAL cText AS STRING

x := 2.0

Y := -(1-x)/(1+x)

YS := Y*Y

//----------- log x = 2* ( Y + Y^3/3 + Y^5/5 + Y^7/7 ......... ) for all x ---------

Sum := 0

FOR n := 1 UPTO 29 STEP 2

NewT := Math.Pow(Y,n)/n

Sum += NewT

NEXT

LogX := 2.0*Sum

cText := "when x is 2 LogX is :- "+CRLF+CRLF

cText += "Sum the series :- "+LogX:ToString()+CRLF

//MessageBox.Show(cText, "Answer :-")

LogX := Math.Log(2.0)

cText += "use static method :- "+LogX:ToString()+CRLF

MessageBox.Show(cText, "Answer :-")

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

LOCAL Y,YS, Sum AS Double

LOCAL x, T, LogX, Tn, NewT AS Double

LOCAL n AS DWORD

LOCAL cText AS STRING

x := 2.0

Y := -(1-x)/(1+x)

YS := Y*Y

//----------- log x = 2* ( Y + Y^3/3 + Y^5/5 + Y^7/7 ......... ) for all x ---------

T := Y

Sum := T

FOR n := 3 UPTO 29 STEP 2

NewT := T*YS

Sum += NewT/n

T := NewT

NEXT

LogX := 2.0*Sum

cText := "when x is 2 LogX is :- "+CRLF+CRLF

cText += "Sum the series :- "+LogX:ToString()+CRLF

//MessageBox.Show(cText, "Answer :-")

LogX := Math.Log(2.0)

cText += "use static method :- "+LogX:ToString()+CRLF

MessageBox.Show(cText, "Answer :-")

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

PHH Feb.2008


Section 36 - Reflection a Start

Reflection is quite a wide and complex topic, BUT, that should not deter us from taking a look to see what may be available to us in the System.Reflection namespace.

It may be worth you paying a visit to the Microsoft MSDN articles on Reflection. The following is a snippet of what I found after Googling - I came across the Reflection Overview page in MSDN.

The examples shown in the following sections will deal with bullet point 5 and the line nearly at the bottom referring to Serialization.

Go to the next sub-section for a start into Reflection - as simple a start as it maybe.

PHH Feb. 2008


Simple Example

Earlier in the section on reading and dumping data from and to external data files, we discussed the idea of reading in an XML data file so that the data values would appear in a data table, and from there how we might use these input data values to set internal application variables.

Let us take this a bit further in this sub-section, and see how we may access the variables of a class in an 'automatic' sort of fashion.

For now we will place and hold the data values into an array instead of reading them from a data table.

In the Start() function we can place the following lines :-

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

LOCAL oTR AS TestReflection

oTR := TestReflection{}

oTR:DoTest()

oTR:ShowResults()

oTR := NULL_OBJECT

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

Please note we need to include :-

#USING System.Reflection

at the start of our PRG file, and also note the new style syntax of #USING in place of the older USING.

The crux of the example is in the method DoTest() so let us examine this now :-

METHOD DoTest() AS VOID

LOCAL oType AS TYPE

LOCAL Fields AS FieldInfo[]

LOCAL i, nLen, nPosn AS DWORD

LOCAL oField AS FieldInfo

LOCAL cFName, cValue AS STRING

oType := SELF:GetType()

Fields := oType:GetFields()

nLen := Fields:Length

FOR i := 1 UPTO nLen

oField := Fields[i]

cFName := oField:Name

nPosn := AScan(SELF:aNameValues,{|x| x[1] == cFName})

IF nPosn > 0

cValue := SELF:aNameValues[nPosn,2]

oField:SetValue(SELF, cValue)

ENDIF

NEXT

RETURN

The code snippet "Self:GetType()" actually finds suitable information for the class variables which are declared as Export variables.

Let us see the class declaration to see which these are :-

CLASS TestReflection

EXPORT Surname AS STRING

EXPORT Add1 AS STRING

EXPORT Add2 AS STRING

EXPORT PCode AS STRING

PROTECT aNameValues AS ARRAY

So only the first four will have information retrieved by this line of code.

Once the oType object has been instantiated it is then possible to use the GetFields() method to make an array of the field information for these export variables.

Obviously, in this case the array is of length four.

For each field we can examine and use the 'name' property, and also change the value of each with the SetValue() method.

Look at the SDK for FieldInfo class to follow this in more detail.

Now lets us see this code running :-

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

By adapting this code slightly we may assign initial test values and then access these by reflection before we set the values using those stored in the array.

Let us now see this adapted code :-

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

So to summarise, we can use this simple reflection coding to gain access to export class variables, find their names, and get and set their values. All without previously knowing their names etc.

It should now be possible for us to add into this example the part where we get the data from a data table which has been filled from an XML file pair.

PHH Feb. 2008


More Advanced Example

For this example I must thank Paul Piko for his contributory code - many thanks. He sent this when he saw me struggling with earlier simple attempts.

But since the purpose of Click Start is to explore syntax and ways of doing things in the new dotNet world, don't think in terms of inferior and superior ways of doing things. It is the most appropriate at the time that really matters, and that changes depending on many circumstances.

However, since I have changed a lot of just basic stuff, I have to take all the blame for any errors and inaccuracies, while Paul gets the credit for the clever contribution :-)

For this more advanced example we can write out the export variable values directly into an XML data file on disk.

The image below is what we get from the first part of the code which will be shown and discussed very shortly.

Note there is one file made in folder C:\VDNUK\ and with a filename of 'person.xml', and this has the structure shown below.

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

For this code we need to make sure that the two lines are included thus :-

#USING System.Xml

#USING System.IO

And now in my testing Start() function I had the code :-

LOCAL oldPerson AS Person

LOCAL newPerson AS Person

LOCAL serial AS Serialization.XmlSerializer

LOCAL oSW AS StreamWriter

LOCAL cFile AS STRING

cFile := "C:\VDNUK\person.xml"

oldPerson := Person{}

oldPerson:firstname := "John"

oldPerson:lastname := "Barleycorn"

oldPerson:dob := 1955.8.3

// Save object to XML file

serial := Serialization.XmlSerializer{typeof(Person)}

oSW := StreamWriter{cFile}

serial:Serialize(oSW,oldPerson)

oSW:Close()

Now we can look at the next part which reads the XMl file and assigns the data to the new object called 'newPerson' before showing it in a message box.

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

It would now seem interesting to me to see if this method handles having more data and named data fields than there are export variables to match.

I have therefore done a test where I have made and extra class variable and assigned it a value and then just dumped the date to file.

The data file looks like this :-

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

Now that we have an extra field called "ExtraTest" and with value 'this is extra' we can do as follows :-

I have now commented out the extra field made to create the data file. So when we read it back in there won't be such a sting field.

Lets see what will happen :-

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

As the above image shows the general approach still works. It would appear that any data for fields not appearing in the object being restored, is just ignored.

Lets us see if other fields which do not figure in the data file are ignored likewise :-

Yes, that is okay too. So we appear to have quite a flexible way of dumping and reading object export variable data to file in XML format.

Remember this is just a start of using some aspects of Reflection and the System.Reflection namespace, there will be many more to come.

You should look out for interesting ways in which Reflection can make our coding life easier.

If you are interested in knowing why we are doing all of this, it was because I was exploring new ways of doing the old Ini and Config file thing !

Yes, there will be dotNet ways of tackling this, AND, we ought to look those out and follow them, BUT, it is nice to explore ways of doing things yourself.

Make sure you look through the previous sub-section where we first read an XML pair into a data table.

This does appear to be longwinded compared to that above, BUT, we do get good practice with data tables and their related syntax in Vulcan.

PHH Feb. 2008


FullWorking Code

 Top Previous

#USING System.Windows.Forms

#USING System.Drawing

#USING System.Reflection

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

CLASS TestReflection

EXPORT Surname AS STRING

EXPORT Add1 AS STRING

EXPORT Add2 AS STRING

EXPORT PCode AS STRING

PROTECT aNameValues AS ARRAY

CONSTRUCTOR()

SUPER()

SELF:PostInit()

RETURN

METHOD PostInit() AS VOID

//------------- Values will be read from a Data Table --------------

//------------- and the table read from XML files ------------------

SELF:aNameValues := {}

AAdd(SELF:aNameValues, {"Surname", "Newell"})

AAdd(SELF:aNameValues, {"Add1", "13, Front Passage,"})

AAdd(SELF:aNameValues, {"Add2", "Sidewinder Lane,"})

AAdd(SELF:aNameValues, {"PCode", "NZ13 7QS"})

RETURN

METHOD DoTest() AS VOID

LOCAL oType AS Type

LOCAL Fields AS FieldInfo[]

LOCAL i, nLen, nPosn AS DWORD

LOCAL oField AS FieldInfo

LOCAL cFName, cValue AS STRING

oType := SELF:GetType()

Fields := oType:GetFields()

nLen := Fields:Length

FOR i := 1 UPTO nLen

oField := Fields[i]

cFName := oField:Name

nPosn := AScan(SELF:aNameValues,{|x| x[1] == cFName})

IF nPosn > 0

cValue := SELF:aNameValues[nPosn,2]

oField:SetValue(SELF, cValue)

ENDIF

NEXT

RETURN

METHOD ShowResults() AS VOID

LOCAL cText AS STRING

cText := SELF:Surname+CRLF

cText += SELF:Add1+CRLF

cText += SELF:Add2+CRLF

cText += SELF:PCode+CRLF

MessageBox.Show(cText,"New Values : ")

RETURN

END CLASS

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

#USING System.Xml

#USING System.IO

[STAThreadAttribute];

FUNCTION Start( asCmdLine AS STRING[] ) AS INT

LOCAL nExitCode AS INT

LOCAL oForm AS WarpForm

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

LOCAL oldPerson AS Person

LOCAL newPerson AS Person

LOCAL serial AS Serialization.XmlSerializer

LOCAL oSW AS StreamWriter

LOCAL cFile, cText AS STRING

cFile := "C:\VDNUK\person.xml"

oldPerson := Person{}

oldPerson:firstname := "John"

oldPerson:lastname := "Barleycorn"

oldPerson:dob := 1955.8.3

// Save object to XML file

serial := Serialization.XmlSerializer{typeof(Person)}

oSW := StreamWriter{cFile}

serial:Serialize(oSW,oldPerson)

oSW:Close()

LOCAL oSR AS StreamReader

//Read object from XML file

serial := Serialization.XmlSerializer{typeof(Person)}

oSR := StreamReader{cFile}

newPerson := (Person)serial:Deserialize(oSR)

oSR:Close()

cText := ""

cText += "============================="+CRLF

cText += newPerson:firstname +CRLF

cText += newPerson:lastname +CRLF

cText += newPerson:dob:ToLongDateString()+CRLF

MessageBox.Show(cText, "Advanced dump to file :")

Application.EnableVisualStyles()

Application.DoEvents()

RETURN nExitCode

CLASS Person

EXPORT firstname AS STRING

EXPORT lastname AS STRING

EXPORT dob AS DateTime

END CLASS

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

PHH Feb. 2008