Jeff takes on the challenge of Turbo Vision, the object-oriented toolbox that's part of the Turbo Pascal 6.0 package, starting with his four basic rules for Turbo Vision programming.
November 01, 1991
URL:http://www.drdobbs.com/architecture-and-design/waves-in-what/184408660
Toward the end of July, my oldest nephew Brian came visiting from Chicago, and together we built a one-tube radio. This wasn't difficult; in fact, the density of radio parts in my garage has gotten so high that radios form spontaneously out there if you shake the place up a little. (This happened a lot in California.) So Brian and I shook up a bin containing a 1H4G tube, a toothpaste pump, some octal relay sockets, a couple of variable capacitors, a 50K pot, some wire, and several nine-volt batteries taped together, and a radio happened.
This is heavy stuff to an eight-year-old, and I tried gamely to explain what was going on. He understood how tubes light up (we turned out the garage lights and watched the thin filament glow a mysterious orange in the dark) and I think he understood that an antenna catches radio waves that are passing by and feeds them into the grid of the tube. His understanding of the rest of it (including regeneration and the nature of that cantankerous "tickler" coil) will come in time. But while we tinkered at it, I kept expecting the one question that I knew I simply could not answer, the question that had, 30 years ago, driven me nuts (as well as several fairly knowledgeable adults, including the old man and my weird Uncle Louie, who knew everything) when I first put my own one-tube radio together.
The question never came. To an eight-year-old, there is still magic, and radios make just as much sense as the Teenage Mutant Ninja Turtles. Hence Brian, confronted with the unmistakable reality of radio waves (in the screechy form of KOY-AM, playing the golden hits of the '40s, '50s, '60s, '70s, '80s, and '90s in my WW-II vintage headphones), never thought to ask, Waves in what?"
Waves in What?
Sheesh, boy, now there's a question.
Sure. Think about it: Toss a rock in a pond and circles of waves will fan out from the point of impact, eventually to lap at the opposite shore. They're waves -- in water. Clap your hands and sound waves carry the rhythm to the folks across the room. Those are waves in air. But radio waves can cross the gulfs between galaxies, where there's as close to nothing as anywhere else you could name. Waves in what? The only truthful way to answer that question is to cop out and contend that radio waves aren't really waves at all, they're, well, they're quantum phenomena, which is just God's way of telling us, because I Say So.
Over the past year, an uncomfortable truth has been dawning on me, as I've experimented with objects in different languages and different applications. The truth is that we've been missing something essential in thinking about objects, or, worse, pretending that that something isn't necessary and doesn't exist. The something I speak of is the larger context within which objects are used. In an admittedly loose analogy, if objects are waves, then their context is what they, as waves, are made of.
We've been very quick to shout about how objects are casehardened little nuggets of functionality, totally encapsulated and independent of other program elements. We've bragged about how the coupling between objects and other program elements approaches zero. We've even been extrapolating from our own hype, and claiming that objects will eventually be the software equivalent of TTL integrated circuits (this is a Brad Cox notion to which I'll return later on) and will be available off-the-shelf in hundreds of different standard "packages" that anyone can buy and use. We've been generating all this yahooha without thinking too hard about how objects interact with their context, and how that context shapes and limits what may be done with its objects.
In all but a few DOS OOP languages, an object's context is, in practice, limited to the language that generates the objects. Can you stick a compiled Turbo Pascal object on a disk and hand it to a friend who programs in Turbo C++? It's supposed to be possible (with some restrictions) but hey; judging by the hOOPla, you'd expect it to be easy.
The new release of JPI's TopSpeed Environment allows much greater sharing of objects across languages. JPI has carefully defined a language-independent calling convention, and all languages share a common runtime library. In theory (and I haven't tested this rigorously), any compiled JPI object should be usable from any JPI object-oriented language.
This tells us something about objects that should be obvious -- and yet how soon we forget: An object is no less dependent on its language's runtime library than any compiled subprogram. We can't even write an object to a disk file without elaborate and (to my mind) somewhat shaky fooling-around, because when the object goes to disk, its code doesn't go with it. The code stays in a library module of some kind, and only instance data is written out to a stream. Registration with a stream only allows the object to locate its code in a code segment when the object is read back from disk to memory. Encapsulation here is more a matter of calling rights than any sort of physical bundling-up-in-a-ball, as too many of us have uncritically come to assume.
If we as a community have misunderstood one element of object-oriented technology more than any other, I would have to point to the object hierarchy context as the culprit. This is the origin of that old objection of Scott Guthery (DDJ, December, 1989) that even if you just want a banana, you get the whole gorilla. And if you're dealing with a sizeable object hierarchy, he's right -- you can't necessarily just pick one item off an object hierarchy tree without the risk of bringing along a lot of unexpected baggage. Scott was reminding us that encapsulation includes everything on a line between the root and the particular leaf you instantiate. In other words, once you invoke inheritance, no object is a banana.
Rather, I think it's fair to say that the object is the hierarchy, and that if you link an object into your application, you link in much or most of its hierarchy and all of the hierarchy's assumptions as well. Linkers can only get so smart, and if you make heavy use of polymorphism and virtual methods, little or nothing will be stripped out of the hierarchy at link time. This will be true even if all you intend to use is one or two different classes from the tree.
The logical extreme of hierarchy context is seen in Smalltalk, where there is only one hierarchy, and everything is part of that hierarchy. In Smalltalk, there is only one indivisible gorilla, and an individual class is nothing more than the gorilla wearing a hand puppet. You can watch the puppet and ignore the gorilla, but you must not forget that the gorilla is always there, and that without him, the hand puppet is limp and useless.
Does it sound like I've become a little disenchanted with objects? I suppose I have. The Object Wave is a little more than two years old now, and it came about with a truckful of promises, few of which have been fulfilled. One promise in particular attracted me, and I've come around to the bitter view that we just can't get there from here.
That was the promise of standard, universally usable software mechanisms made possible through object-oriented technology. I first encountered this promise in Brad Cox's very good book, Object-Oriented Programming: an Evolutionary Approach. The book was in one respect an apologia for Cox's own language, Objective C, but it was also a call to produce what Cox calls (and has trademarked as) Software ICs.
For the last 15 or 20 years engineers have been able to make use of a vast array of digital logic blocks created as TTL integrated circuits. The logic blocks are all different, but what remains universally standard is the way the blocks interact. All chips use a standard power potential of five volts, and all input and output pins have a standard voltage swing and current source/sink ability called fan-in and fan-out. Several dozen companies make or have made TTL ICs, and all of them may be used interchangeably, regardless of their vendors.
Why can't we do this with objects? Simply put: There is no standard context. The standard context of TTL ICs is simple and universal: the laws of electrical physics, bounded by a spare handful of standard assumptions about voltage and current values.
The best we can do so far is create standard object libraries for use with particular compilers. Object Professional and Turbo Vision are good examples. But hey, what's the essential difference between object libraries like that and the garden-variety procedure and function libraries we've been using for years? The answer: Not much.
There is promise for creating language-spanning object libraries for JPI's TopSpeed system, but that's no consequence of OOP technology; you could do the same with ordinary procedures and functions.
If there is a solution -- or at least a next step -- it will have to be the broadening of object contexts to embrace the platform, regardless of language. DOS as we know it is hopeless in that regard, because it can only reliably treat a piece of code as an executable stored on disk. (DOS's sole motion toward what I have in mind involves TSRs, which are thoroughly crippled by DOS's careless internal architecture. A TSR object library, while theoretically possible, has to jump through flaming hoops to keep itself from crashing the system. Not cool.) The platform must be able to treat a binary image containing both code and data (that is, an object) as a loadable library that can be made safely available to all transient applications.
Surprise! We're halfway there, and what Microsoft does in the next two years will dictate how close we will eventually come.
The underlying machinery for a language-independent, platform-wide object context has been with us since the release of Windows 3.0. I'm talking about Dynamic Link Libraries (DLLs), perhaps the least-appreciated feature Windows brings us. DLLs are a little like units, and a little like TSRs. Like units, they can contain both code and data, and they can have an initialization section (but not an exit procedure). Like TSRs they can be loaded by Windows and ;left in memory for the use of other programs. They occupy the same ecological niche under Windows that TSRs occupy under DOS: that of resident platform extension. Windows itself is composed of several DLLs that are loaded by the Windows kernel when Windows takes control, so in creating a DLL you are in a sense extending Windows.
What DLLs do not have is any high-level knowledge of object-oriented methods. This is the missing half that must be added, and again, it's a conceptually simple matter of defining some standards. There is a movement underway at Microsoft to define some of these platform-wide, object-interface standards, but one gets the impression that real technology is still years off. I suspect it probably wont be incorporated into a Microsoft platform before the 32-bit Windows descendant expected (by this columnist at least) no earlier than September of 1993.
Once a tenable platform object context appears as Win32, a great deal more of the promise of OOP should become real. We should be able to pass "canned" objects from machine to machine on disk or over networks, and expect them to work identically on all Win32-based machines, from any language that supports the standard object messaging protocol.
That path wont get us to the non-Win32 machines, but shall we say this doesn't distress me. At the rate big hardware companies like Apple and IBM continue to slit their own throats, I anticipate that by the turn of the century the Gruesome Twosome will be reduced to a couple of niche firms offering special-purpose, high-end boxes and paying their bills by selling Pacific Rim 80686-based PC clones. Nope. Software rules the future, and you and I both know who rules software, with no serious challenger in sight.
I said all that as my way of announcing that I am ceasing my search for object-oriented truth and will instead settle for a good toolbox. Because that's where I've found object technology to really shine: in the design of software tools that may be easily extended and customized without gross rewriting of source code.
Good examples are beginning to appear, now that developers have digested and understand object technology. For the next several columns I'll be probing Turbo Vision, which comes in the can with Turbo Pascal 6.0 and is probably the most-owned (if not necessarily the most-used) object-oriented toolbox going right now.
Turbo Vision has taken a lot of heat on the networks and in the user groups since it appeared. It's quite literally unlike anything Borland has ever released for Turbo Pascal, and it really does represent a new turn in technology, not only for Turbo Pascal but for Pascal in general. A lot of people hate it. Few people truly understand it. But I would like to plead for everyone to see it on its own terms and give it a fair chance.
I'm not really stealing a phrase from Chapter 1 of the Turbo Vision Guide when I say that Turbo Vision provides the bones of a windowing application. I wrote some early parts of the Guide, and I like that way of putting it: Turbo Vision allows you to inherit the bones of an application (things such as menus, windows, edit fields, and so on) to which you add the meat, which are the routines that allow the application to do your specific tasks.
It isn't quite accurate to call TV a user-interface toolbox. It's really a boilerplate application, stripped of all application-specific functionality. The most visible portions of TV are UI components, obviously -- but behind the scenes are some other remarkable mechanisms as well. Turbo Vision contains a very efficient event manager that lets you break away from the "pick a number from the menu" kind of hierarchical control that leads to what I called The Cuba Lake Effect a few columns back. All of this is built into a remarkable object type called TApplication, which is the boilerplate application I spoke of earlier.
From a height, this is how you use Turbo Vision: You create a child type of TApplication. You override some of its existing methods and add some new methods; add a few other objects, define some menus and dialogs, and hook the various parts together with pointers. That's your application; and your main program looks like this in almost every case:
The first line sets your application up. The second line runs it. The third line reasserts system defaults, deallocates memory, and otherwise tidies up whatever mess the application made.
I won't be so bold as to say it's simple, nor that it's easy. Turbo Vision was certainly the most difficult learning experience I ever had in Pascal, so if you're having trouble with TV, don't kick yourself for it. Nonetheless, for all the trouble I've had with it (and am still having with it!) I continue to use it and like it more and more as I do.
I see Turbo Vision as something like a pair of expensive leather boots. They hurt when you first put them on, but as you wear them two things happen: The boots adapt to your feet, and your feet adapt to the boots. You need to use TV intensely for a while to make it stop hurting -- because the hurt comes from not truly understanding what the damned thing is up to, nor how to make it do what you want. As you become familiar with Turbo Vision, however, the pain goes away, not only because you understand how it works, but also because understanding how it works allows you to bend it in your own directions to suit your own needs. Over time, it becomes a far better fit -- and eventually, you'll wonder how you ever did without it.
I've managed to distill some guidelines for working with Turbo Vision. These are the "rules," in a sense, and if you're not willing to play by the rules you'll be in a rut and wearing the tread off your tires in no time.
Regardless of how willing you are to work on Turbo Vision's own terms, there remains the question of how to learn it. Turbo Vision is hard to learn in part because it's one big, heavily integrated mechanism and not a loose bin of software odds and ends. It's tough to pull one element of TV up for examination without having to understand seven hundred other things first.
This is what I call "looking for the front door;" it's the search for a starting point on a sequential path to mastery of the product.
There's no easy front door to learning TV, and that sequential path is inevitably going to be cluttered with forward references. As with anything else, you'll learn best by doing. Here's my suggested strategy:
Listing One (page 143) contains an early version of my own TV learning app, HCALC.PAS. HCALC ("HouseCalc") is intended to be a collection of utilities for dealing with home ownership. Listing One only implements a simple mortgage calculator, allowing you to create several mortgage scenarios in independent windows and compare them.
I created it in very small stages. I had a menu bar and status line with dummy commands before the commands did anything. The first windows were empty windows. I created the menu option to close all windows before the windows contained any information. Only then did I actually create the dialog box to gather mortgage parameters, and the last thing I did was actually place the mortgage information into the windows. The application was compilable and executable at every stage, which greatly helped me assimilate the knowledge that I was drinking from the TV fire hose.
It worked for me. It should work for you. Try it! In the next several columns I'll be explaining the operation of HCALC in detail. You might make a Xerox copy of Listing One so that you can refer to it in the coming months, because we won't be reprinting HCALC in every issue.
I haven't forgotten my original goal of designing and building a data communications application in this column. We're working on it. There's no easy path to the best goals, and if it takes a year, it takes a year. I'm not going anywhere. Stay tuned.
Copyright © 1991, Dr. Dobb's Journal
Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.
To Everything it's Context
The Language ContexT
The Hierarchy Context
Toward a Platform Context
Halfway There
On to Turbo Vision
Meat and Bones
BEGIN
MyApplication.Init;
MyApplication.Run;
MyApplication.Done;
END.
The Pain and the Gain
An unpleasant chap on CompuServe referred to Turbo Vision as Nazi programming. This is just another manifestation of Not Invented Here, and if he persists in spending all his days creating his own personal event-driven environment, he's welcome to it. I personally enjoy the freedom from having to solve such problems myself.
The Learning Curve
My Learning App
_STRUCTURED PROGRAMMING COLUMN_
by Jeff Duntemann
[LISTING ONE]
PROGRAM HCalc; { By Jeff Duntemann; Update of 10/31/91 }
{ Requires Turbo Pascal 6.0! }
USES App,Dialogs,Objects,Views,Menus,Drivers,
FInput, { By Allen Bauer; on CompuServe BPROGA }
Mortgage; { By Jeff Duntemann; from DDJ 10/91 }
CONST
cmNewMortgage = 199;
cmExtraPrin = 198;
cmCloseAll = 197;
cmCloseBC = 196;
cmPrintSummary = 195;
WindowCount : Integer = 0;
TYPE
MortgageDialogData =
RECORD
PrincipalData : Real;
InterestData : Real;
PeriodsData : Integer;
END;
ExtraPrincipalDialogData =
RECORD
PaymentNumber : Integer;
ExtraDollars : Real;
END;
THouseCalcApp =
OBJECT(TApplication)
InitDialog : PDialog; { Dialog for initializing a mortgage }
ExtraDialog : PDialog; { Dialog for entering extra principal }
CONSTRUCTOR Init;
PROCEDURE InitMenuBar; VIRTUAL;
PROCEDURE CloseAll;
PROCEDURE HandleEvent(VAR Event : TEvent); VIRTUAL;
PROCEDURE NewMortgage;
END;
PMortgageTopInterior = ^TMortgageTopInterior;
TMortgageTopInterior =
OBJECT(TView)
Mortgage : PMortgage;
CONSTRUCTOR Init(VAR Bounds : TRect);
PROCEDURE Draw; VIRTUAL;
END;
PMortgageBottomInterior = ^TMortgageBottomInterior;
TMortgageBottomInterior =
OBJECT(TScroller)
{ Points to Mortgage object owned by TMortgageView }
Mortgage : PMortgage;
CONSTRUCTOR Init(VAR Bounds : TRect;
AHScrollBar, AVScrollbar : PScrollBar);
PROCEDURE Draw; VIRTUAL;
END;
PMortgageView = ^TMortgageView;
TMortgageView =
OBJECT(TWindow)
Mortgage : TMortgage;
CONSTRUCTOR Init(VAR Bounds : TRect;
ATitle : TTitleStr;
ANumber : Integer;
InitMortgageData :
MortgageDialogData);
PROCEDURE HandleEvent(Var Event : TEvent); VIRTUAL;
PROCEDURE ExtraPrincipal;
PROCEDURE PrintSummary;
DESTRUCTOR Done; VIRTUAL;
END;
CONST
DefaultMortgageData : MortgageDialogData =
(PrincipalData : 100000;
InterestData : 10.0;
PeriodsData : 360);
VAR
HouseCalc : THouseCalcApp; { This is the application object itself }
{------------------------------}
{ METHODS: THouseCalcApp }
{------------------------------}
CONSTRUCTOR THouseCalcApp.Init;
VAR
R : TRect;
aView : PView;
BEGIN
TApplication.Init; { Always call the parent's constructor first! }
{ Create the dialog for initializing a mortgage: }
R.Assign(20,5,60,16);
InitDialog := New(PDialog,Init(R,'Define Mortgage Parameters'));
WITH InitDialog^ DO
BEGIN
{ First item in the dialog box is input line for principal: }
R.Assign(3,3,13,4);
aView := New(PFInputLine,Init(R,8,DRealSet,DReal,0));
Insert(aView);
R.Assign(2,2,12,3);
Insert(New(PLabel,Init(R,'Principal',aView)));
{ Next is the input line for interest rate: }
R.Assign(17,3,26,4);
aView := New(PFInputLine,Init(R,6,DRealSet,DReal,3));
Insert(aView);
R.Assign(16,2,25,3);
Insert(New(PLabel,Init(R,'Interest',aView)));
R.Assign(26,3,27,4); { Add a static text "%" sign }
Insert(New(PStaticText,Init(R,'%')));
{ Up next is the input line for number of periods: }
R.Assign(31,3,36,4);
aView := New(PFInputLine,Init(R,3,DUnsignedSet,DInteger,0));
Insert(aView);
R.Assign(29,2,37,3);
Insert(New(PLabel,Init(R,'Periods',aView)));
{ These are standard buttons for the OK and Cancel commands: }
R.Assign(8,8,16,10);
Insert(New(PButton,Init(R,'~O~K',cmOK,bfDefault)));
R.Assign(22,8,32,10);
Insert(New(PButton,Init(R,'Cancel',cmCancel,bfNormal)));
END;
{ Create the dialog for adding additional principal to a payment: }
R.Assign(20,5,60,16);
ExtraDialog := New(PDialog,Init(R,'Apply Extra Principal to Mortgage'));
WITH ExtraDialog^ DO
BEGIN
{ First item in the dialog is the payment number to which }
{ we're going to apply the extra principal: }
R.Assign(9,3,18,4);
aView := New(PFInputLine,Init(R,6,DUnsignedSet,DInteger,0));
Insert(aView);
R.Assign(3,2,12,3);
Insert(New(PLabel,Init(R,'Payment #',aView)));
{ Next item in the dialog box is input line for extra principal: }
R.Assign(23,3,33,4);
aView := New(PFInputLine,Init(R,8,DRealSet,DReal,2));
Insert(aView);
R.Assign(20,2,35,3);
Insert(New(PLabel,Init(R,'Extra Principal',aView)));
{ These are standard buttons for the OK and Cancel commands: }
R.Assign(8,8,16,10);
Insert(New(PButton,Init(R,'~O~K',cmOK,bfDefault)));
R.Assign(22,8,32,10);
Insert(New(PButton,Init(R,'Cancel',cmCancel,bfNormal)));
END;
END;
{ This method sends out a broadcast message to all views. Only the
{ mortgage windows know how to respond to it, so when cmCloseBC is
{ issued, only the mortgage windows react--by closing. }
PROCEDURE THouseCalcApp.CloseAll;
VAR
Who : Pointer;
BEGIN
Who := Message(Desktop,evBroadcast,cmCloseBC,@Self);
END;
PROCEDURE THouseCalcApp.HandleEvent(VAR Event : TEvent);
BEGIN
TApplication.HandleEvent(Event);
IF Event.What = evCommand THEN
BEGIN
CASE Event.Command OF
cmNewMortgage : NewMortgage;
cmCloseAll : CloseAll;
ELSE
Exit;
END; { CASE }
ClearEvent(Event);
END;
END;
PROCEDURE THouseCalcApp.NewMortgage;
VAR
Code : Integer;
R : TRect;
Control : Word;
ThisMortgage : PMortgageView;
InitMortgageData : MortgageDialogData;
BEGIN
{ First we need a dialog to get the intial mortgage values from }
{ the user. The dialog appears *before* the mortgage window! }
WITH InitMortgageData DO
BEGIN
PrincipalData := 100000;
InterestData := 10.0;
PeriodsData := 360;
END;
InitDialog^.SetData(InitMortgageData);
Control := Desktop^.ExecView(InitDialog);
IF Control <> cmCancel THEN { Create a new mortgage object: }
BEGIN
R.Assign(5,5,45,20);
Inc(WindowCount);
{ Get data from the initial mortgage dialog: }
InitDialog^.GetData(InitMortgageData);
{ Call the constructor for the mortgage window: }
ThisMortgage :=
New(PMortgageView,Init(R,'Mortgage',WindowCount,
InitMortgageData));
{ Insert the mortgage window into the desktop: }
Desktop^.Insert(ThisMortgage);
END;
END;
PROCEDURE THouseCalcApp.InitMenuBar;
VAR
R : TRect;
BEGIN
GetExtent(R);
R.B.Y := R.A.Y + 1; { Define 1-line menu bar }
MenuBar := New(PMenuBar,Init(R,NewMenu(
NewSubMenu('~M~ortgage',hcNoContext,NewMenu(
NewItem('~N~ew','F6',kbF6,cmNewMortgage,hcNoContext,
NewItem('~E~xtra Principal ','',0,cmExtraPrin,hcNoContext,
NewItem('~C~lose all','F7',kbF7,cmCloseAll,hcNoContext,
NewItem('E~x~it','Alt-X',kbAltX,cmQuit,hcNoContext,
NIL))))),
NIL)
)));
END;
{---------------------------------}
{ METHODS: TMortgageTopInterior }
{---------------------------------}
CONSTRUCTOR TMortgageTopInterior.Init(VAR Bounds : TRect);
BEGIN
TView.Init(Bounds); { Call ancestor's constructor }
GrowMode := gfGrowHiX; { Permits pane to grow in X but not Y }
END;
PROCEDURE TMortgageTopInterior.Draw;
VAR
YRun : Integer;
Color : Byte;
B : TDrawBuffer;
STemp : String[20];
BEGIN
Color := GetColor(1);
MoveChar(B,' ',Color,Size.X); { Clear the buffer to spaces }
MoveStr(B,' Principal Interest Periods',Color);
WriteLine(0,0,Size.X,1,B);
MoveChar(B,' ',Color,Size.X); { Clear the buffer to spaces }
{ Here we convert payment data to strings for display: }
Str(Mortgage^.Principal:7:2,STemp);
MoveStr(B[2],STemp,Color); { At beginning of buffer B }
Str(Mortgage^.Interest*100:7:2,STemp);
MoveStr(B[14],STemp,Color); { At position 14 of buffer B }
Str(Mortgage^.Periods:4,STemp);
MoveStr(B[27],STemp,Color); { At position 27 of buffer B }
WriteLine(0,1,Size.X,1,B);
MoveChar(B,' ',Color,Size.X); { Clear the buffer to spaces }
MoveStr(B,
' Extra Principal Interest',
Color);
WriteLine(0,2,Size.X,1,B);
MoveChar(B,' ',Color,Size.X); { Clear the buffer to spaces }
MoveStr(B,
'Paymt # Prin. Int. Balance Principal So far So far ',
Color);
WriteLine(0,3,Size.X,1,B);
END;
{------------------------------------}
{ METHODS: TMortgageBottomInterior }
{------------------------------------}
CONSTRUCTOR TMortgageBottomInterior.Init(VAR Bounds : TRect;
AHScrollBar, AVScrollBar :
PScrollBar);
BEGIN
{ Call ancestor's constructor: }
TScroller.Init(Bounds,AHScrollBar,AVScrollBar);
GrowMode := gfGrowHiX + gfGrowHiY;
Options := Options OR ofFramed;
END;
PROCEDURE TMortgageBottomInterior.Draw;
VAR
Color : Byte;
B : TDrawBuffer;
YRun : Integer;
STemp : String[20];
BEGIN
Color := GetColor(1);
FOR YRun := 0 TO Size.Y-1 DO
BEGIN
MoveChar(B,' ',Color,80); { Clear the buffer to spaces }
Str(Delta.Y+YRun+1:4,STemp);
MoveStr(B,STemp+':',Color); { At beginning of buffer B }
{ Here we convert payment data to strings for display: }
Str(Mortgage^.Payments^[Delta.Y+YRun+1].PayPrincipal:7:2,STemp);
MoveStr(B[6],STemp,Color); { At beginning of buffer B }
Str(Mortgage^.Payments^[Delta.Y+YRun+1].PayInterest:7:2,STemp);
MoveStr(B[15],STemp,Color); { At position 15 of buffer B }
Str(Mortgage^.Payments^[Delta.Y+YRun+1].Balance:10:2,STemp);
MoveStr(B[24],STemp,Color); { At position 24 of buffer B }
{ There isn't an extra principal value for every payment, so }
{ display the value only if it is nonzero: }
STemp := '';
IF Mortgage^.Payments^[Delta.Y+YRun+1].ExtraPrincipal > 0
THEN
Str(Mortgage^.Payments^[Delta.Y+YRun+1].ExtraPrincipal:10:2,STemp);
MoveStr(B[37],STemp,Color); { At position 37 of buffer B }
Str(Mortgage^.Payments^[Delta.Y+YRun+1].PrincipalSoFar:10:2,STemp);
MoveStr(B[50],STemp,Color); { At position 50 of buffer B }
Str(Mortgage^.Payments^[Delta.Y+YRun+1].InterestSoFar:10:2,STemp);
MoveStr(B[64],STemp,Color); { At position 64 of buffer B }
{ Here we write the line to the window, taking into account the }
{ state of the X scroll bar: }
WriteLine(0,YRun,Size.X,1,B[Delta.X]);
END;
END;
{------------------------------}
{ METHODS: TMortgageView }
{------------------------------}
CONSTRUCTOR TMortgageView.Init(VAR Bounds : TRect;
ATitle : TTitleStr;
ANumber : Integer;
InitMortgageData :
MortgageDialogData);
VAR
TopInterior : PMortgageTopInterior;
BottomInterior : PMortgageBottomInterior;
HScrollBar,VScrollBar : PScrollBar;
R,S : TRect;
BEGIN
TWindow.Init(Bounds,ATitle,ANumber); { Call ancestor's constructor }
{ Call the Mortgage object's constructor using dialog data: }
WITH InitMortgageData DO
Mortgage.Init(PrincipalData,
InterestData / 100,
PeriodsData,
12);
{ Here we set up a window with *two* interiors, one scrollable, one }
{ static. It's all in the way that you define the bounds, mostly: }
GetClipRect(Bounds); { Get bounds for interior of view }
Bounds.Grow(-1,-1); { Shrink those bounds by 1 for both X & Y }
{ Define a rectangle to embrace the upper of the two interiors: }
R.Assign(Bounds.A.X,Bounds.A.Y,Bounds.B.X,Bounds.A.Y+4);
TopInterior := New(PMortgageTopInterior,Init(R));
TopInterior^.Mortgage := @Mortgage;
Insert(TopInterior);
{ Define a rectangle to embrace the lower of two interiors: }
R.Assign(Bounds.A.X,Bounds.A.Y+5,Bounds.B.X,Bounds.B.Y);
{ Create scroll bars for both mouse & keyboard input: }
VScrollBar := StandardScrollBar(sbVertical + sbHandleKeyboard);
{ We have to adjust vertical bar to fit bottom interior: }
VScrollBar^.Origin.Y := R.A.Y; { Adjust top Y value }
VScrollBar^.Size.Y := R.B.Y - R.A.Y; { Adjust size }
{ The horizontal scroll bar, on the other hand, is standard: }
HScrollBar := StandardScrollBar(sbHorizontal + sbHandleKeyboard);
{ Create bottom interior object with scroll bars: }
BottomInterior :=
New(PMortgageBottomInterior,Init(R,HScrollBar,VScrollBar));
{ Make copy of pointer to mortgage object: }
BottomInterior^.Mortgage := @Mortgage;
{ Set the limits for the scroll bars: }
BottomInterior^.SetLimit(80,InitMortgageData.PeriodsData);
{ Insert the interior into the window: }
Insert(BottomInterior);
END;
PROCEDURE TMortgageView.HandleEvent(Var Event : TEvent);
BEGIN
TWindow.HandleEvent(Event);
IF Event.What = evCommand THEN
BEGIN
CASE Event.Command OF
cmExtraPrin : ExtraPrincipal;
cmPrintSummary : PrintSummary;
ELSE
Exit;
END; { CASE }
ClearEvent(Event);
END
ELSE
IF Event.What = evBroadcast THEN
CASE Event.Command OF
cmCloseBC : Done
END; { CASE }
END;
PROCEDURE TMortgageView.ExtraPrincipal;
VAR
Control : Word;
ExtraPrincipalData : ExtraPrincipalDialogData;
BEGIN
{ Execute the "extra principal" dialog box: }
Control := Desktop^.ExecView(HouseCalc.ExtraDialog);
IF Control <> cmCancel THEN { Update the active mortgage window: }
BEGIN
{ Get data from the extra principal dialog: }
HouseCalc.ExtraDialog^.GetData(ExtraPrincipalData);
Mortgage.Payments^[ExtraPrincipalData.PaymentNumber].ExtraPrincipal :=
ExtraPrincipalData.ExtraDollars;
Mortgage.Recalc; { Recalculate the amortization table... }
Redraw; { ...and redraw the mortgage window }
END;
END;
PROCEDURE TMortgageView.PrintSummary;
BEGIN
END;
DESTRUCTOR TMortgageView.Done;
BEGIN
Mortgage.Done; { Dispose of the mortgage object's memory }
TWindow.Done; { Call parent's destructor to dispose of window }
END;
BEGIN
HouseCalc.Init;
HouseCalc.Run;
HouseCalc.Done;
END.
[THE FOLLOWING IS SOURCE FOR FINPUT.PAS]
unit FInput;
{$X+}
{
This unit implements a derivative of TInputLine that supports several
data types dynamically. It also provides formatted input for all the
numerical types, keystroke filtering and uppercase conversion, field
justification, and range checking.
When the field is initialized, many filtering and uppercase converions
are implemented pertinent to the particular data type.
The CheckRange and ErrorHandler methods should be overridden if the
user wants to implement then.
This is just an initial implementation and comments are welcome. You
can contact me via Compuserve. (76066,3202)
I am releasing this into the public domain and anyone can use or modify
it for their own personal use.
Copyright (c) 1990 by Allen Bauer (76066,3202)
1.1 - fixed input validation functions
This is version 1.2 - fixed DataSize method to include reals.
fixed Draw method to not format the data
while the view is selected.
}
interface
uses Objects, Drivers, Dialogs;
type
VKeys = set of char;
PFInputLine = ^TFInputLine;
TFInputLine = object(TInputLine)
ValidKeys : VKeys;
DataType,Decimals : byte;
imMode : word;
Validated, ValidSent : boolean;
constructor Init(var Bounds: TRect; AMaxLen: integer;
ChrSet: VKeys;DType, Dec: byte);
constructor Load(var S: TStream);
procedure Store(var S: TStream);
procedure HandleEvent(var Event: TEvent); virtual;
procedure GetData(var Rec); virtual;
procedure SetData(var Rec); virtual;
function DataSize: word; virtual;
procedure Draw; virtual;
function CheckRange: boolean; virtual;
procedure ErrorHandler; virtual;
end;
const
imLeftJustify = $0001;
imRightJustify = $0002;
imConvertUpper = $0004;
DString = 0;
DChar = 1;
DReal = 2;
DByte = 3;
DShortInt = 4;
DInteger = 5;
DLongInt = 6;
DWord = 7;
DDate = 8;
DTime = 9;
DRealSet : VKeys = [#1..#31,'+','-','0'..'9','.','E','e'];
DSignedSet : VKeys = [#1..#31,'+','-','0'..'9'];
DUnSignedSet : VKeys = [#1..#31,'0'..'9'];
DCharSet : VKeys = [#1..#31,' '..'~'];
DUpperSet : VKeys = [#1..#31,' '..'`','{'..'~'];
DAlphaSet : VKeys = [#1..#31,'A'..'Z','a'..'z'];
DFileNameSet : VKeys = [#1..#31,'!','#'..')','-'..'.','0'..'9','@'..'Z','^'..'{','}'..'~'];
DPathSet : VKeys = [#1..#31,'!','#'..')','-'..'.','0'..':','@'..'Z','^'..'{','}'..'~','\'];
DFileMaskSet : VKeys = [#1..#31,'!','#'..'*','-'..'.','0'..':','?'..'Z','^'..'{','}'..'~','\'];
DDateSet : VKeys = [#1..#31,'0'..'9','/'];
DTimeSet : VKeys = [#1..#31,'0'..'9',':'];
cmValidateYourself = 5000;
cmValidatedOK = 5001;
procedure RegisterFInputLine;
const
RFInputLine : TStreamRec = (
ObjType: 20000;
VmtLink: Ofs(typeof(TFInputLine)^);
Load: @TFInputLine.Load;
Store: @TFinputLine.Store
);
implementation
uses Views, MsgBox, StrFmt, Dos;
function CurrentDate : string;
var
Year,Month,Day,DOW : word;
DateStr : string[10];
begin
GetDate(Year,Month,Day,DOW);
DateStr := SFLongint(Month,2)+'/'
+SFLongInt(Day,2)+'/'
+SFLongInt(Year mod 100,2);
for DOW := 1 to length(DateStr) do
if DateStr[DOW] = ' ' then
DateStr[DOW] := '0';
CurrentDate := DateStr;
end;
function CurrentTime : string;
var
Hour,Minute,Second,Sec100 : word;
TimeStr : string[10];
begin
GetTime(Hour,Minute,Second,Sec100);
TimeStr := SFLongInt(Hour,2)+':'
+SFLongInt(Minute,2)+':'
+SFLongInt(Second,2);
for Sec100 := 1 to length(TimeStr) do
if TimeStr[Sec100] = ' ' then
TimeStr[Sec100] := '0';
CurrentTime := TimeStr;
end;
procedure RegisterFInputLine;
begin
RegisterType(RFInputLine);
end;
constructor TFInputLine.Init(var Bounds: TRect; AMaxLen: integer;
ChrSet: VKeys; DType, Dec: byte);
begin
if (DType in [DDate,DTime]) and (AMaxLen < 8) then
AMaxLen := 8;
TInputLine.Init(Bounds,AMaxLen);
ValidKeys:= ChrSet;
DataType := DType;
Decimals := Dec;
Validated := true;
ValidSent := false;
case DataType of
DReal,DByte,DLongInt,
DShortInt,DWord : imMode := imRightJustify;
DChar,DString,
DDate,DTime : imMode := imLeftJustify;
end;
if ValidKeys = DUpperSet then
imMode := imMode or imConvertUpper;
EventMask := EventMask or evMessage;
end;
constructor TFInputLine.Load(var S: TStream);
begin
TInputLine.Load(S);
S.Read(ValidKeys, sizeof(VKeys));
S.Read(DataType, sizeof(byte));
S.Read(Decimals, sizeof(byte));
S.Read(imMode, sizeof(word));
S.Read(Validated, sizeof(boolean));
S.Read(ValidSent, sizeof(boolean));
end;
procedure TFInputLine.Store(var S: TStream);
begin
TInputLine.Store(S);
S.Write(ValidKeys, sizeof(VKeys));
S.Write(DataType, sizeof(byte));
S.Write(Decimals, sizeof(byte));
S.Write(imMode, sizeof(word));
S.Write(Validated, sizeof(boolean));
S.Write(ValidSent, sizeof(boolean));
end;
procedure TFInputLine.HandleEvent(var Event: TEvent);
var
NewEvent: TEvent;
begin
case Event.What of
evKeyDown : begin
if (imMode and imConvertUpper) <> 0 then
Event.CharCode := upcase(Event.CharCode);
if not(Event.CharCode in [#0..#31]) then
begin
Validated := false;
ValidSent := false;
end;
if (Event.CharCode <> #0) and not(Event.CharCode in ValidKeys) then
ClearEvent(Event);
end;
evBroadcast: begin
if (Event.Command = cmReceivedFocus) and
(Event.InfoPtr <> @Self) and
((Owner^.State and sfSelected) <> 0) and
not(Validated) and not(ValidSent) then
begin
NewEvent.What := evBroadcast;
NewEvent.InfoPtr := @Self;
NewEvent.Command := cmValidateYourself;
PutEvent(NewEvent);
ValidSent := true;
end;
if (Event.Command = cmValidateYourself) and
(Event.InfoPtr = @Self) then
begin
if not CheckRange then
begin
ErrorHandler;
Select;
end
else
begin
NewEvent.What := evBroadCast;
NewEvent.InfoPtr := @Self;
NewEvent.Command := cmValidatedOK;
PutEvent(NewEvent);
Validated := true;
end;
ValidSent := false;
ClearEvent(Event);
end;
end;
end;
TInputLine.HandleEvent(Event);
end;
procedure TFInputLine.GetData(var Rec);
var
Code : integer;
begin
case DataType of
Dstring,
DDate,
DTime : TInputLine.GetData(Rec);
DChar : char(Rec) := Data^[1];
DReal : val(Data^, real(Rec) , Code);
DByte : val(Data^, byte(Rec) , Code);
DShortInt : val(Data^, shortint(Rec) , Code);
DInteger : val(Data^, integer(Rec) , Code);
DLongInt : val(Data^, longint(Rec) , Code);
DWord : val(Data^, word(Rec) , Code);
end;
end;
procedure TFInputLine.SetData(var Rec);
begin
case DataType of
DString,
DDate,
DTime : TInputLine.SetData(Rec);
DChar : Data^ := char(Rec);
DReal : Data^ := SFDReal(real(Rec),MaxLen,Decimals);
DByte : Data^ := SFLongInt(byte(Rec),MaxLen);
DShortInt : Data^ := SFLongInt(shortint(Rec),MaxLen);
DInteger : Data^ := SFLongInt(integer(Rec),MaxLen);
DLongInt : Data^ := SFLongInt(longint(Rec),MaxLen);
DWord : Data^ := SFLongInt(word(Rec),MaxLen);
end;
SelectAll(true);
end;
function TFInputLine.DataSize: word;
begin
case DataType of
DString,
DDate,
DTime : DataSize := TInputLine.DataSize;
DChar : DataSize := sizeof(char);
DReal : DataSize := sizeof(real);
DByte : DataSize := sizeof(byte);
DShortInt : DataSize := sizeof(shortint);
DInteger : DataSize := sizeof(integer);
DLongInt : DataSize := sizeof(longint);
DWord : DataSize := sizeof(word);
else
DataSize := TInputLine.DataSize;
end;
end;
procedure TFInputLine.Draw;
var
RD : real;
Code : integer;
begin
if not((State and sfSelected) <> 0) then
case DataType of
DReal : begin
if Data^ = '' then
Data^ := SFDReal(0.0,MaxLen,Decimals)
else
begin
val(Data^, RD, Code);
Data^ := SFDReal(RD,MaxLen,Decimals);
end;
end;
DByte,
DShortInt,
DInteger,
DLongInt,
DWord : if Data^ = '' then Data^ := SFLongInt(0,MaxLen);
DDate : if Data^ = '' then Data^ := CurrentDate;
DTime : if Data^ = '' then Data^ := CurrentTime;
end;
if State and (sfFocused+sfSelected) <> 0 then
begin
if (imMode and imRightJustify) <> 0 then
while (length(Data^) > 0) and (Data^[1] = ' ') do
delete(Data^,1,1);
end
else
begin
if ((imMode and imRightJustify) <> 0) and (Data^ <> '') then
while (length(Data^) < MaxLen) do
insert(' ',Data^,1);
if (imMode and imLeftJustify) <> 0 then
while (length(Data^) > 0) and (Data^[1] = ' ') do
delete(Data^,1,1);
end;
TInputLine.Draw;
end;
function TFInputLine.CheckRange: boolean;
var
MH,DM,YS : longint;
Code : integer;
MHs,DMs,YSs : string[2];
Delim : char;
Ok : boolean;
begin
Ok := true;
case DataType of
DDate,
DTime : begin
if DataType = DDate then Delim := '/' else Delim := ':';
if pos(Delim,Data^) > 0 then
begin
MHs := copy(Data^,1,pos(Delim,Data^));
DMs := copy(Data^,pos(Delim,Data^)+1,2);
delete(Data^,pos(Delim,Data^),1);
YSs := copy(Data^,pos(Delim,Data^)+1,2);
if length(MHs) < 2 then MHs := '0' + MHs;
if length(DMs) < 2 then DMs := '0' + DMs;
if length(YSs) < 2 then YSs := '0' + YSs;
Data^ := MHs + DMs + YSs;
end;
if (length(Data^) >= 6) and (pos(Delim,Data^) = 0) then
begin
val(copy(Data^,1,2), MH, Code);
if Code <> 0 then MH := 0;
val(copy(Data^,3,2), DM, Code);
if Code <> 0 then DM := 0;
val(copy(Data^,5,2), YS, Code);
if Code <> 0 then YS := 0;
if DataType = DDate then
begin
if (MH > 12) or (MH < 1) or
(DM > 31) or (DM < 1) then Ok := false;
end
else
begin
if (MH > 23) or (MH < 0) or
(DM > 59) or (DM < 0) or
(YS > 59) or (YS < 0) then Ok := false;
end;
insert(Delim,Data^,5);
insert(Delim,Data^,3);
end
else
Ok := false;
end;
DByte : begin
val(Data^, MH, Code);
if (Code <> 0) or (MH > 255) or (MH < 0) then Ok := false;
end;
DShortint :
begin
val(Data^, MH, Code);
if (Code <> 0) or (MH < -127) or (MH > 127) then Ok := false;
end;
DInteger :
begin
val(Data^, MH, Code);
if (Code <> 0) or (MH < -32768) or (MH > 32767) then Ok := false;
end;
DWord : begin
val(Data^, MH, Code);
if (Code <> 0) or (MH < 0) or (MH > 65535) then Ok := false;
end;
end;
CheckRange := Ok;
end;
procedure TFInputLine.ErrorHandler;
var
MsgString : string[80];
Params : array[0..1] of longint;
Event: TEvent;
begin
fillchar(Params,sizeof(params),#0);
MsgString := '';
case DataType of
DDate : MsgString := ' Invalid Date Format! Enter Date as MM/DD/YY ';
DTime : MsgString := ' Invalid Time Format! Enter Time as HH:MM:SS ';
DByte,
DShortInt,
DInteger,
DWord : begin
MsgString := ' Number must be between %d and %d ';
case DataType of
DByte : Params[1] := 255;
DShortInt : begin Params[0] := -128; Params[1] := 127; end;
DInteger : begin Params[0] := -32768; Params[1] := 32768; end;
DWord : Params[1] := 65535;
end;
end;
end;
MessageBox(MsgString, @Params, mfError + mfOkButton);
end;
end.