Waves in What?

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

NOV91: STRUCTURED PROGRAMMING

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.

To Everything it's Context

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.

The Language ContexT

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.

The Hierarchy Context

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.

Toward a Platform Context

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.

Halfway There

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.

On to Turbo Vision

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.

Meat and Bones

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:

BEGIN
   MyApplication.Init;
   MyApplication.Run;
   MyApplication.Done;
END.

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.

The Pain and the Gain

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.

  1. Understand pointers. Really. I mean, really really. If you aren't comfortable with pointers, TV will be a brick wall ten miles high. Polymorphism depends utterly on pointers to make it work, and TV makes the most pervasive use of polymorphism that I have ever encountered.
  2. Don't try to subset it. You can't just pull a menu procedure out of Turbo Vision and use it apart from TApplication. TV isn't really a toolbox from which you can pick one gizmo or the other. It's a boilerplate application, and the explicit and implicit coupling among the components is very high. About the only thing I would say is extractable from Turbo Vision is the TCollection hierarchy, which is basically a linked-list manager that functions tolerably well on its own.
  3. Don't try to modify it. In other words, extend it but don't try to alter the look or operation of the parts that are already there. The coupling among the components is high, and this coupling is not simply a matter of procedure calls or global variables. There are a multitude of very subtle assumptions underlying Turbo Vision, (most of them completely undocumented) and even innocuous-appearing changes can have completely unexpected consequences in what seem to be totally unrelated parts of the system.
  4. Do things the Turbo Vision way. Whenever you can, let Turbo Vision carry the ball its own way and in its own direction. All of us have our own ways of thinking about program design, and what is easy to forget when using Turbo Vision is that your program is already designed. The event-driven architecture embodied in TApplication is complete and functional, and it will shape everything else your program does from top to bottom. Don't fight it. The thing was created to save you time, and if you persist in trying to twist it in some direction that aligns with your own biases, you'll be wasting huge amounts of time and energy.
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

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:

  1. Start by reading the Turbo Vision tutorials in Part 1 of the Turbo Vision Guide. Pull up the demonstration apps as you go, run them, and see if you can make any sense at all of the code. (This sort of experience is cumulative. Eventually the Aha! Insight! epiphanies will come so quickly your head will spin.) Much of it won't make sense, and a lot of what may seem to make sense at first won't stick. Don't worry -- and don't get discouraged. Just keep going.
  2. Before you begin tinkering with TV itself, read up on and experiment with the TCollection class and its children. These stand independent of TV, and can be learned and used without any knowledge of TV.
  3. Read the rest of TV Guide Part 2, which is a detailed description of Turbo Vision. The first time through, this will be rough going. Again, bull through it at least once, and twice if you have the intestinal fortitude.
  4. Take one of the example apps and begin changing it, one thing at a time. Start small. Change the wording on a window title. Add a dummy menu item. Add a dummy command to the status line. Each time you make a change, crank your brain wide open to the place in the big picture where your one small change fits in. This is the stage where you have to try to pull all your previous undigested knowledge together. If you have the leisure, I'd suggest spending two or three full days doing nothing else; or failing that, a solid week of evenings.
  5. Specify a simple application of your own, and try to make it happen. Steal freely from the example apps. They work well, and they were written by people who will probably always know more about TV than you will. Try to shape your learning app such that it can be added to incrementally, allowing you to test and learn from it as you go, in small chunks. Don't try to make 1500 lines of TV code compile at once, the first time.

My Learning App

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.


_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.


Copyright © 1991, Dr. Dobb's Journal

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.