This file attempts to give a brief once over of areas that trouble Pascal programmers when working with Turbo Vision and TVGraphic. (TV is used when referring to both.)
Contents
Basics on Pointers in TV
PStrings and Memory Overwrites
TV/TVGraphic - Order of Views
Inserting Views - Assumptions, Where, How
Color Palettes
Communication Among Views in a Dialog Box (TDialog)
Three things combine to make the transition to Turbo Vision and similar environments difficult - Object orientation, event driven behavior and pointers. Tutorials on objects, event driven environments and pointers are in Borland's doc but thinking Turbo Vision "pointer" isn't. That's here.
The reward for learning Turbo Vision and TVGraphic is the high quality of programs you can turn out. There is far more to them than just a pretty face. Menus, windows and screen redraws are handled fully automatically. The event driven framework is sophisticated and flexible. Skim the Borland manuals on Collections, Streams and Resources. These are very powerful data handling tools and they are integrated in. No longer must files contain records of the same length unless working at the binary level. Collections provide this benefit and more for items in memory. Much of this new Pascal power comes from pointers.
BASICS ON POINTERS IN TV
In much traditional Pascal programming, the idea was to avoid using pointers unless absolutely necessary. This was a virtue to the extent that it avoided the many bugs and system crashes which pointers lend themselves to. However pointers are the only way to access the true power of object orientation in Pascal and C++. Only a pointer to an object can allow you to write code that works with any object type a user can select at run time. Yes, Pascal object pointers are typed, but an object pointer can point to an object of its own type or to an object of a descendant type. Your code will work with either at run time. See the section of the Pascal manuals that has a general tutorial on pointers.
If you haven't worked with pointers and look at the source code for Turbo Vision, parts of it will be almost unrecognizable. In fact these parts of Turbo Vision don't feel as if they were written by a Pascal programmer - more like a pointer maniac.
Let's look at ways of thinking that may help.
A pointer is just the numerical address (location) of a data structure (char,integer,record,object instance) in memory. It is the way programs, at the internal level, actually know where they put their variables in memory.
Object Types (definitions) are usually called "objects" in Pascal (they are called classes in other object oriented languages). An object type is similar to a Record in that it contains data fields but it also defines Functions and Procedures which are contained in itself. Then there are actual individual object variables, each created in its own space in memory. These are also called objects. Confusing. Finally there can be a pointer type defined for any particular object type. Pascal will enforce type checking to be sure all these things match.
Turbo Vision speak:
1. Assume in TV that there is a pointer type defined for every object type. (Follow this practice yourself.)
TView - T indicates an object type (a class)
PView - P indicates a pointer type. PView type pointers point to TView type objects (and their descendants).
example
type {Pascal allows this out of order declaration
within a single Type statement}
PView = ^TView
TView = Object
Owner : PGroup;
Size : TPoint; {TPoint is a record with fields
x,y}
...
...
end;
! NOTE THE ^ SYMBOL. It is Critical to Understanding !
With pointers and variables (and their types), the ^ lets
you grab one if you know the name of the other.
^Variable is a pointer to Variable
Pointer^ is the variable that Pointer points to.
Think about this for a minute.
2. Whenever you see a variable representing an object in Turbo Vision, EXPECT it is a pointer to the object (rather than the object itself). This is correct 90-99% of the time.
example
var
MyView : PView;
x : integer; begin
MyView := New(PView, Init(.......));
x := MyView^.Size.x;
MyView is a pointer variable - commonly called a pointer. The first statement after Begin creates an object of type TView in memory and then assigns the address of this memory to pointer MyView.
Turbo Vision's TView has a field Size which contains a record with x and y fields.
So "Size.x" by itself should look familiar.
What may not look familiar is MyView^ .
And how about MyView^.Owner^.Size.x ?
Welcome to Turbo Vision and the land of pointer.
Take a deep breath. Exhale.
Ingest your favorite mood altering substances.
Say: It will all make sense.
We said MyView^ meant the variable (object) pointed at by MyView
so MyView^.Size
says that Size is a field in variable (object) MyView^.
Remember MyView^ is an object of type TView.
(Think of the object as a Record with various fields)
In TV, each TView has a field Owner which is of type PGroup.
TGroups are descended from TViews.
then MyView^.Owner^.Size
will be the Size field of the Owner^ (TV jargon) of MyView^.
That is, Owner is a PGroup pointer. So Owner^ is another
object of a type descended from TView.
It therefore has ITS OWN Size field!
THAT Size field is: MyView^.Owner^.Size !
3. See > MyView^.Owner^.Size
Think > MyView's Owner's Size
Since Size is just a record type variable, the x field of Size is as usual - Size.x
Again, MyView and Owner are the pointers, MyView^ and Owner^ are the actual objects.
If you can start to think as in 3 above, you are over the hardest part.
Take a Break
Continuing
HOW TO CRASH YOUR COMPUTER, two ways, no sweat
MyView^.Owner^.Size
When you initialize an object in TV, all the fields are set to 0 (Magic done in TObject.Init). This includes pointer variables like Owner in MyView/TView. This is referred to as a nil pointer - it doesn't point at anything real.
BUT it still acts as a pointer. If Owner is nil and your code attempts to read Owner^.Size.x, it will find a value. Not one you can predict but not usually dangerous.
However there are functions and procedures attached to a TView object and it is often necessary to call them through an object's pointer. It is very common for TV code to call say
Owner^.MoveTo
Which tells object Owner^ to execute its MoveTo procedure (where MoveTo is a procedure defined in object type TView). If pointer Owner is nil, the call proceeds anyway (the compiler knows the type of Owner and so can find most of its methods). But if MoveTo is a "virtual" method (see Borland Doc) or if MoveTo in turn calls any virtual methods, then Pascal will jump to the method based on a table of procedure starting addresses near address nil=0 of memory. Wherever an object's virtual method table is, it is guaranteed not to be there. Boom.
>>> Calls to virtual methods of uninitialized objects can be trapped as a runtime error if the compiler's Range checking is turned on.
The other common way to crash also comes from pointer fields in objects. Say you sometimes create a TCollection and assign it to the pointer MyView.CollectionPtr and sometimes not. When you call MyView.Done, you need to Dispose of the TCollection. If you don't check whether the pointer to TCollection is nil, sometimes you will try to dispose a nil pointer. Boom.
The only real protection is to be methodical about testing whether pointers are set to nil before calling "Pointer^.procedure" or disposing pointers.
4.A. If you have a pointer in one object to another object, assign a valid value to this pointer in the Init constructor if possible. (It isn't always possible/desirable.) If you leave the pointer = nil in MyView.Init, you must later be perfect in testing whether the pointer is still nil. Turn RangeChecking on for the first month or two.
4.B. In your Done methods, ALWAYS check for a nil pointer before Disposing objects you created but didn't insert into the view. Otherwise you will find that a front panel Reset switch on your computer is very important in the learning phase of Turbo Vision.
As a Pascal programmer you are just not used to thinking like this. So save your source code to file BEFORE you run. After a while this diligence in assigning values to pointers and testing for nil becomes automatic. Sounds like C, no?
5. Pointer Safety in Construction
{note: a TGroup is a descendent of TView}
var
MyGroup : PView; {can point to any descendent of TView}
begin
.....
.....
> MyGroup := New(PGroup, Init(.......)); <
!!! ALWAYS do Construction as above !!!
Using the NEW function in the special way above combines allocating the memory for a new object with calling its constructor Init to initialize the object and build its virtual method table.
Yes there are other ways.
New(MyGroup, Init(.........));
Problem : Assuming you wanted a TGroup, you just constructed
and initialized a TView since MyGroup is of type TView. What-
ever type pointer MyGroup is will determine what type of object
you will construct. If the variable declarations are not on
screen, it is easy to forget what type MyGroup really is.
MyGroup := New(PGroup);
MyGroup^.Init(........);
NEVER - if you forget to call .Init before you call a method of
MyGroup, could bomb - Reset switch time.
Aside - There must be a way around all these pointers.
Now it has occurred to some of you (it did to me) that one can avoid some of the pointer experience when using standalone objects by doing
var
MyObj : TView; {TView is object, NOT a pointer type}
at the beginning of a subroutine. The subroutine creates an object of type TView called MyObj and will dispose of it as the subroutine ends.
This works fine but you can still get caught if you forget to call MyObj.Init BEFORE you call ANY of the object's methods. If Range checking is off and you call a virtual method, directly or indirectly, then Boom. If Range checking is on, then runtime error if virtual method but no error if ordinary method. Still your data fields have not been initialized (happens in Init) and if they contain an object pointer, it won't be set to nil. Your safety tests for nil pointers will fail - you could end up disposing a pointer in your Done method that had never been assigned. Boom.
Break
Easy stuff
How about the ^Variable usage?
In Turbo Vision it is common to organize your variables (objects) by creating either a linked list of pointers to the variables or
an array of pointers to the variables. This is convenient because it is also usual to create objects as the user needs them (like a File Open dialog box) and then destroy them to regain their memory. If all possible dialogs and other objects had memory allocated at program startup, it would be restrictive given the large size of many of today's DOS programs.
So the objects are scattered through memory (unlike an array of objects which would be ordered) but their sizes don't all have to be the same (as for an array of a datatype). Same on disk files. So back to pointers.
The ^Variable usage is much less common than the Pointer^ usage. This is simply because most object variables are already pointers in TV.
APointer := ^AnyVariable;
assigns the address of AnyVariable to APointer.
Also you see this all the time in type declarations:
type
PMyView = ^TMyView;
Which declares PMyView to be a pointer to type TMyView.
Turbo Vision has alot of procedures that take pointers to strings as a parameter rather than the string itself. It goes against the Pascal grain but that's the way it is. So you will probably use a string and assign it a value in the usual way. Then pass the string as a pointer to the procedure:
type
Str20 = string[20]; definition
PString = ^String; definition
procedure DoIt(StrPtr : PString); definition
begin
end;
....
Str20 := 'AARDVARK';
DoIt(Str20); {type mismatch error}
DoIt(^Str20); {simple as that}
If you have stuck it out this far, congratulations. Class dismissed.
{-------------------------------}
PSTRINGS and MEMORY OVERWRITES
TV often handles strings by using a pointer to them. It often organizes groups of strings as Collections of PStrings. For example it uses the following function to create PStrings:
function NewStr(S : string) : PString;
So AStrPointer := NewStr('a string')
allocates memory for the string "a string" and returns a pointer to this memory. The amount of memory allocated is just enough to contain the exact string you pass to the function plus one byte for the string's length. Very efficient memory usage.
This is fine until you want to change the contents of the string.
If you create a regular Pascal string
MyString : string = 'Longer than a string';
and then assign MyString to the string pointed to by AStrPointer
AStrPointer^ := MyString;
you Will OverWrite Memory not belonging to AStrPointer! MyString is too long to fit into the memory allocated for AStrPointer but it will be put there anyway. Notice that you have no foolproof way to know at run time how big the memory for AStrPointer is even though Pascal's memory manager knows. Hopefully Borland will come up with something better.
There are no error messages or compiler checks.
Note: if you want to assign AStrPointer^ a shorter string or one of the same length, you should check first to see if AStrPointer is nil. ( AStrPointer is set to nil if NewStr is passed an empty string.)
>> Think twice before changing the contents/length of a string pointer.
{-------------------------------}
TV/TVGraphic ORDER OF VIEWS
The Borland doc never really gets around to nailing down view order. We'll try it here. TV refers to both Turbo Vision and TVGraphic.
TGroup's contain TViews. For the purposes of discussion here, a TView is anything visible on the screen. TGroups are not visible except for the Views they contain. The Application (TGroup) contains the Menu, StatusLine and DeskTop. The DeskTop (TGroup) contains a visible Background and the windows (TWindow - descends from TGroup) on screen. These windows contain TViews although in TVGraphic they may also contain TSubWindows, a descendant of TGroup. When thinking of view order, remember that every visible part of TV is a TView.
There are two areas that depend on the order of views. One is the handling of events and the other is the drawing of views on the screen.
Terminology
Z- order : partially defined concept in Borland doc to explain
view order
function TGroup.First
Borland = "returns a pointer to the TGroup's first subview, the one closest to the top in Z-order."
The FIRST subview gets passed keyboard and command events before any other subview. Visually it is the top view on the screen - all other views appear behind it. In Turbo Vision, the First subview draws first (counter-intuitive as that may seem). In TVGraphic, the First subview draws last. A subview newly inserted using TGroup.Insert becomes the First subview.
function TView.NextView
Returns a pointer to the "Next" subview in the view's Owner. So if you start with the First subview. You can chain through all the subview NextView pointers to reach the Last subview.
function TView.PrevView
Returns a pointer to the "Previous" view. So you could start at the Last view and chain through all the PrevView subview pointers to the First view. Opposite direction from NextView.
TGroup.Last pointer field
Points to the Last subview in order. This view receives events last. Draws first in TVGraphic. Draws last in Turbo Vision.
Top
When discussing Z order, the First subview is at the top.
Bottom
When discussing Z order, the Last subview is at the bottom.
Note that the subview list is circular. The TView.Next field of the Last subview points at the First subview. The TView.Prev function of the First subview returns a pointer to the Last subview.
{----------------------------}
INSERTING VIEWS - Assumptions, Where, How
TV is based around certain assumptions about how you will organize your program. You don't have to follow these assumptions but it sure makes life easier.
The first assumption is that you will insert all non-window views into a window or a descendant type of a window (dialog). Not into the DeskTop or Application.
The second assumption is that you will insert all dialogs into the DeskTop. (ExecuteDialog does this automatically for example).
The third assumption is that you will only insert buttons, inputlines, listboxes, labels (objects defined in unit Dialogs that are selectable or cause other objects to be selected) in a dialog.
Well what if you don't want to?
First Assumption
In general, mixing selectable views with more than one window in the desktop will interfere with moving between windows using F6, Shift F6. See further discussion in section on TVGraphic's TPanWindow in main doc file.
Also inserting views outside the above assumptions will usually cause them to draw in different colors since they map into a different part of the color palette. For example. a dialog's palette has many more entries than a TWindow, including entries to TButtons. If you insert a button into your descendant of TWindow, there are no TWindow palette entries defined for it. You will have to fix that.
Second Assumption
Note : Don't try to insert a dialog into a dialog.
Modal Dialogs: the only two sane choices are inserting it into the DeskTop (DeskTop.ExecView or TProgram.ExecuteDialog) or into the Application. Use the DeskTop.
Non-modal dialogs (persistent dialogs}
Insert into the DeskTop (DeskTop^.Insert). TVGraphic also allows insertion of non-modal dialogs into a TPanWindow.
Third Assumption
If you want a button in your DeskTop, make it non-selectable by clearing its sfSelectable bit in its State field. If you want a bank of selectable buttons, think about placing them in a special window. Then insert that window in the DeskTop. For other dialog controls you are on your own. See discussion under First Assumption.
{----------------------------}
COLOR PALETTES
TV's color palette system is the part of TV that this programmer finds the most cryptic. There is a discussion on changing palette colors in the main doc file. Here we look at concepts.
Note: It is often easier to avoid using the palette when writing Draw methods for new views that don't resemble standard TV views. Work back toward the palette method of obtaining color as your application firms up.
Each type of view has what is called a color palette. Although palettes are Pascal String types, it is best to think of them as arrays of bytes. Only the TApplication has actual colors in its palette. All the other palettes contain numbers that represent offsets into the palette (array) of the view's Owner. The application palette packs two four bit colors into each byte. Foreground color is the lower 4 bits, background is the upper four bits.
In TV, the color a view will use to draw itself depends on what type of view it is inserted into. Example: the background color of StaticText differs depending on whether it is inserted into a window or a dialog. To make this happen, each view's Draw method calls the view's GetColor function to find out what colors to use. GetColor calls its Owner's GetColor recursively until it reaches the Application.
As GetColor works it way up the view chain, it calls GetPalette at each level and sums the offset value from each palette to get the total offset into the Application palette. (The Desktop GetPalette returns nil - it doesn't have a palette so the offset values GetColor has calculated are not changed by the DeskTop.)
Pragmatically:
TV2.0 "GrayDialog" and TV1.0 Dialogs:
The offset in the first palette entry is 32 (valid entries range from 1 up). So anything inserted into a TDialog will have the offsets in its own palette increased by 31.
(A subview palette offset of 1 will map to the first dialog palette entry of 32. So 32-1 = 31.)
For a "BlueWindow"
The offset in the first palette entry is 8. So anything inserted into a "BlueWindow" (actually gray in TVGraphic) will have the offsets in its own palette increased by 7.
If you insert a window in a window for example, then entries will be increased by 7 by each window's GetPalette. This is a source of much confusion since you no longer map to the expected colors. Note: TVGraphic TSubwindows and TVGraphic non-modal TDialog avoid this problem by using modified GetPalette functions.
Example:
TInputLine inserted into a Dialog The second entry in the TInputLine palette is for "Active".
Its offset is 19. (Active means the window TInputLine is
inserted into is in the Active state.)
Draw obtains the active colors by calling GetColor(2).
(2 for second entry in TIputLine's palette)
To calculate the offset into the application's Palette,
add 31 (from Dialog) to 19 = 50; The colors are the 50th
entry in TApplication's palette.
{----------------------------}
COMMUNICATION AMONG VIEWS IN A DIALOG BOX (TDIALOG)
Many View types can be included in a Dialog box and it is helpful to consider the interview communication that takes place. The summary below is true for TVGraphic and is generally true for TV.
Remember - all predefined Dialog boxes in TV are ExecView'd by the Desktop rather than Inserted - this makes them "modal". This means that the Dialog box fetches all Events from the Event queue rather than the Application fetching them. So Event handling begins in TDialog.HandleEvent.
Note that when a Dialog box is modal, there is no real difference in a subView using the Message function to call its Owner and sending a PutEvent - both events end up in TDialog.HandleEvent. (Of course a Message is processed immediately, a PutEvent goes to the rear of the Event queue.) Often the event is of type evBroadcast so it will reach all the Dialog's views in case more than one needs to react.
BUT if a Dialog box is Not Modal, an event sent with PutEvent would first show up at TProgram.HandleEvent. If it was an evBroadcast, it would be received by EVERY view in the application!
Note that EndModal returns the terminating command through ExecView or ExecuteDialog.
Summary of commands and messages:
TDialog.HandleEvent(Event)
if AllowBothMButtons is false (TVGraphic default) then