Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Design

Structured Programming


SEP92: STRUCTURED PROGRAMMING

STRUCTURED PROGRAMMING

Downstream

Jeff Duntemann, KG7JF

I'll not be telling any tales this issue, in courtesy to poor Jon Erickson, who has to figure out how to fit nearly 600 lines of listing in back of the mag this month. Brevity is a necessity this time.

The listing is the final, streamable version of HCALC. From the Mortgage menu you can save the top mortgage window as a stream file, and load a named mortgage file back from disk into a window. It works beautifully: In late April I refinanced our house and used HCALC mercilessly to compare payments and accelerated payoff schedules. It's great fun to add a few hundred dollars to your next payment and see five or ten payments vanish off the back end of the mortgage--and a powerful incentive to pay the damned thing off completely and own the place free and clear.

Registering Things

I went over the process of registering types for stream I/O last month. HCALC.PAS contains stream-registration records for all the non-Borland types that need to be put out to streams. It also contains a procedure, RegisterAllTypes, that does all the necessary program registering.

Using the Standard Dialogs

One thing HCALC does now that it didn't do before is make use of Borland's standard file-open dialog. The standard dialogs are a sort of "free gift" that came along with Turbo Pascal 6.0. They're not mentioned anywhere in the documentation, and to figure them out you have to read the source code for the STDDLG.PAS unit and the code for the example programs that use the standard dialogs defined in STDDLG.PAS.

When you get the chance, I encourage you to print out and read STDDLG.PAS closely. It's only 1400 lines or so, and it's a wonderful demonstration of some of Turbo Vision's more arcane features. The TFileList class is a great lesson in the use of broadcast messages. It communicates which file is currently selected in a list of files by broadcasting the fact that a new file has been selected--and then inviting any message receiver to inquire as to the name of the file in question. Neat trick--one you might want to use yourself someday.

There are other things in STDDLG.PAS that can be lifted out and used independently of either TV or OOP: a Contains function that quickly tells whether characters in one string are present in another, and an Equal function that allows you to test the first n characters of two strings for case-insensitive equality. Not a big deal codewise--but they're there; canned, labeled, and ready to eat.

Using the standard file-open dialog is a snap. You don't have to allocate space for the dialog before you use it. You allocate it, execute it, and destroy it all in one statement, which in most cases will look a lot like the statement in Example 1.

Example 1: Using the standard file-open dialog.

  VAR
    FileName : FNameStr;

    FileName := '*.MTG';
    IF ExecDialog (New(PFileDialog,
                   Init('*.*', 'Load Mortgage File',
                         '~N~ame', fdOpenButton, 100)),
                  @FileName) <> cmCancel
    THEN { You have a filename, so do
            something with it: }
      BEGIN
      ...
      END

ExecDialog creates a dialog object on the heap with New, calls its constructor to set it up, and then executes it. You pass to the dialog the address of a variable (here, FileName) that will contain the filename that the dialog selects. Once you close the dialog box, ExecDialog automatically removes it from memory. Very slick.

The name of the file-open dialog is misleading; it indirectly returns the name of a selected file but does not actually open the file. You have to do that. In fact, what the file-open dialog returns is one of Turbo Vision's predefined command constants. If the command is cmCancel, you know that the user pressed the Cancel button on the dialog. Otherwise, you can assume that a filename was in fact selected.

The Problems of an Object Web

Once you use the file-open dialog to select a filename, you've got the problem of doing one of two things with it: reading the file stored on disk somewhere under that name as a stream, or writing some object or objects under that name to disk as a stream. And before we do this, we had better confront the reality of what we're trying to stuff onto that stream.

Figure 1 is a conceptual diagram of a TMortgageView object. The relationships among the parts are critical to getting the whole thing onto and off of a stream intact. The TMortgageView object contains a TMortgage object as one of its fields. The TMortgage object, in turn, contains a pointer to a variable-length array of records that contains the mortgage amortization table. (I covered the details of the TMortgage object last month.)

The TMortgageView object is associated with two other objects defined in HCALC.PAS: TMortgageTopInterior and TMortgageBottomInterior. TMortgageView is a group, and as a group it "contains" one instance of each of the two interior objects. These interior objects are instantiated and added to the TMortgageView group in TMortgageView's constructor Init, using the Insert method.

So far, so good. There is a further complication: Each of the two interior objects has its own pointer to the TMortgage object contained within TMortgageView as a field.

This all works, and within Turbo Vision's somewhat Byzantine view of the world, it all makes sense. The weblike nature of the TMortgageView object makes it tough to conceptualize how this thing is put onto a stream. As usual, explaining it is a lot harder than actually doing it. For ample evidence of that, see the TMortgageView.Store method in Listing One (page 158). It's only four lines long.

Storing to a Stream is Easy

The TMortgage object knows how to put itself out onto the stream, as we saw last month. Ditto the TMortgageView object, which inherits streamability from its parent class, TWindow.

So the first thing you do is allow the TWindow.Store method to store the TWindow-ness of TMortgageView onto the stream. That done, you must store any additional fields added by the TMortgageView definition.

Mostly, that's TMortgage. You tell the stream to put TMortgage onto itself, and the stream will call TMortgage's Store method to do, in a sense, the actual putting. I explained this complication two issues ago; it kept me scratching my head for a good long time.

Both TMortgageTopInterior and TMortgageBottomInterior rely entirely on their parent types to store them to a stream, since they add no new fields (but only methods) to the definition of their parent types. Better still, the group that owns the interior objects takes care of telling the interior objects to put themselves out to the stream, using the hidden machinery contained in the group object.

Two lines remain in TMortgageView.Store:

PutSubViewPtr(S,BottomInterior);
PutSubViewPtr(S,TopInterior);

These are a little tougher to explain.

Subviews and Subview Pointers

Note from Figure 1 that TMortgageView has its own pointers to the two interior objects. And remember that these two pointers are pointers added within the definition of TMortgageView, and have nothing to do with the completely separate connections that exist between a group and the views that it owns. A group can call certain methods in the views that it owns--subviews--but the group can't call the object-specific methods that you add to the views you define. Worse, this behind-the-scenes access works only when the group wants it to work, not when you want it to work. If you want to call a certain method in a view owned by a group, you need your own connection to that view.

Such a connection to a subview is called a subview pointer, and a subview pointer is exactly what BottomInterior and TopInterior are. Each is a pointer defined in the group object (here, TMortgageView) that points to one of the views that the group owns.

These connections are part of the state of the TMortgageView object, and they need to be saved out to the stream somehow. Keep in mind that we're not storing the interior objects to the stream here. TMortgageView owns the objects and will take care of putting them onto the stream behind the scenes. You have to be sure that your own connections to the subviews don't get mixed up in the process.

The method TGroup.PutSubViewPtr doesn't store the subview pointer itself, but an entry that indicates which subview the pointer points to, referenced against the group's own internal subview list. PutSubViewPtr says, in effect, "Store the private ID number of the subview to which this pointer points." This may seem unnecessary when storing a complex object to a stream, but it gets real damned important later on when you want to bring the object back.

Bringing an Object Back from a Stream

Order is the main challenge here. One terrific way to go wrong is to bring things back from the stream in an order different from the way they went out to the stream.

If the first thing we stored was the TWindow elements of TMortgageView, that should be the first thing to come back. (We're looking at TMortgageView.Load here.) The TMortgage object must be the next thing to come off the stream. Remember that the TMortgage.Load method called by the stream behind the scenes is a constructor, and can fully instantiate the object without having to call TMortgage.Init.

The TMortgage object comes in from the stream to a temporary copy allocated on the heap. Streams always read from disk to the heap; they're built that way. Once on the heap, the heap-based TMortgage object can be copied to its rightful place inside the TMortgageView object with Move.

Now the dicey stuff starts. Although we have no immediate evidence of it, in calling TWindow.Load, all the TMortgageView's subviews were already loaded from the stream. So the subviews are out there already, connected to their parent group through the parent's private connections. Our own connections to the subviews do not yet exist.

These connections are the pointers TopInterior and BottomInterior, and we derive these pointers from the stream using the TGroup.GetSubViewPtr method. Earlier, we saved the group's private ID codes for its subviews onto the stream. GetSubViewPtr converts these private ID codes to actual pointers to the subviews represented by those ID codes. Otherwise, we'd have no unambiguous way to derive a pointer to either of the subviews.

Don't make the two calls to GetSubViewPtr in the opposite order that you made the calls to PutSubViewPtr when you wrote the object to the stream. I made that mistake and it earned me some truly bizarre behavior--order counts, big-time!

Finally, we give each of the subviews its own pointer to the TMortgage object. You can see these pointers in the arrows in Figure 1. Each subview accesses the TMortgage object for its own purposes, and both need pointers to the TMortgage object. These pointers don't need to come from the stream; once we have pointers to the subviews, we can just use the address-of operator to generate pointers to the TMortgage instance within the TMortgageView instance.

Whew!

If all of that makes your head spin, don't feel bad. It took me a couple of months of loose moments to figure it all out and get it right, and some people consider me a pretty bright guy.

Some pointers on the actual HCALC.PAS code: For space and clarity reasons I didn't put any error handling in THouseCalcApp's LoadMortgage, and StoreMortgage methods. If either method can't connect with the specified file for some reason, it just exits without giving the user another chance. If you intend to build on HCALC, it would pay to add some means to alert the user to a problem with the disk and give them a chance to correct the problem (say, trying to read a nonexistent file) and try again.

Now that you understand streams (I hope), you might be able to take a sensible step and store the two dialog boxes out as resources. Read up on resources in the TV documentation; they're essentially random-access streams in which you can stash any standard object type for fast retrieval. A fair amount of code in HCALC is taken up building the two dialogs, and you can cut the size of the program down considerably by converting the dialogs to resources. This is the method used by the excellent Turbo Vision Developer's Toolkit from Blaise Computing (Berkeley, California).

An Exercise for the Reader

One thing I haven't done with HCALC (because it would make the listing too large to comfortably print in the magazine) is provide a feature that saves the current state of the HCALC desktop out to a stream when you exit the program. This is what people mean when they speak of "saving the program to a stream." If you have a number of mortgage windows open and positioned at various points in their amortization tables, you can save the state of every mortgage window exactly as it exists when you exit HCALC. That way, the next time you run HCALC, it can automatically load the desktop--and all your mortgage windows--just as they were when you packed it all in and went to bed.

The way to do this isn't exactly to save the application to the stream. That wouldn't be necessary, because it's the TDesktop type--not TApplication--that owns all the windows you open on the desktop. If you save the desktop, you save everything between the menu bar and status line, including all opened windows and the background.

So to demonstrate your mastery of Turbo Vision, add all necessary code to HCALC to save the desktop to a stream or restore it from a stream. Here are some hints:

  • Add methods that save and restore the desktop to your application-specific child class of TApplication. The save and restore methods should work with the DeskTop predefined global variable, which is a pointer that points to the current desktop object.
  • Define commands for saving and restoring the desktop, and add code to identify and service these commands in the HandleEvent method of your application object.
  • Add code that issues the save and restore commands where you want. One place would be the Mortgage menu, but the obvious place is the code that processes the cmQuit command.
TProgram.HandleEvent calls EndModal(cmQuit) to end the program. Create a shell method in your application type that intercepts cmQuit through the application's HandleEvent method, saves the desktop to the stream, and then calls EndModal(cmQuit). The place to load the desktop, pretty obviously, is in the application's constructor. Be sure to call DeskTop^.DrawView to make the newly loaded desktop visible.

Turbo Vision Wrap

With all that accomplished, I think I'm going to set Turbo Vision aside for a while. As powerful a platform as it is, there are plenty of times when I mess with it for a few hours and feel more than a little chewed on. Still, it does a hell of a lot of work for you, and provides two particular "soft" benefits:

  • It provides a level of conceptual compatibility with the Windows GUI on machines incapable of running Windows. If people can use your Turbo Vision application, they won't suffer a great deal learning an eventual Windows port.
  • It gives you the programmer a taste for Turbo Pascal for Windows programming. The code itself isn't especially compatible between the two platforms for a number of reasons, but if you can grasp the ideas behind Turbo Vision, you'll slide right into TPW like a bearing into a greased sleeve. On the other hand, if you have tried and abhor Turbo Vision (and judging by my recent mail, a fair number of people do) TPW will send your agony meter off the scale.
The problem, as I've hinted before, is this: To get good at either Turbo Vision or TPW, you must use them relentlessly, all the time, forever and ever amen. There are just too many essential little details, rules, exceptions, and nonsystematic hassles to keep it all in the back of your head if you get maybe two hours to program every Sunday night.

Which doesn't mean I don't endorse them both. I do. But in addition to people who program for a living, there are people who program to support their other work, part-time and as required. This sort of programming is every bit as worthy as the all-the-time kind, but it requires tools that do much more of the work and (most important of all) manage much more of the underlying complexity of the platform.

A lot of new and intriguing products have piled up while we've been wrestling with Turbo Vision, and it's high time to haul them out and get a look at them. I also think it's time to do a little more exploring of easier ways to program for Microsoft Windows, and simpler ways to implement databases in Pascal. Stay tuned.



_STRUCTURED PROGRAMMING COLUMN_
by Jeff Duntemann


[LISTING ONE]
<a name="0208_000f">

PROGRAM HCalc;   { By Jeff Duntemann; Update of 5/2/92 }
                 { Requires Turbo Pascal 6.0! }
USES App,Dialogs,Objects,Views,Menus,StdDlg,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;
  cmLoadMortgage = 194;
  cmSaveMortgage = 193;

  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;
      PROCEDURE   LoadMortgage;
      PROCEDURE   SaveMortgage;
    END;
  PMortgageTopInterior = ^TMortgageTopInterior;
  TMortgageTopInterior =
    OBJECT(TView)
      Mortgage       : PMortgage;
      CONSTRUCTOR Init(VAR Bounds : TRect);
      CONSTRUCTOR Load(VAR S : TStream);
      PROCEDURE   Store(VAR S : TStream);
      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);
      CONSTRUCTOR Load(VAR S : TStream);
      PROCEDURE   Store(VAR S : TStream);
      PROCEDURE   Draw; VIRTUAL;
    END;

  PMortgageView = ^TMortgageView;
  TMortgageView =
    OBJECT(TWindow)
      Mortgage    : TMortgage;
      TopInterior    : PMortgageTopInterior;
      BottomInterior : PMortgageBottomInterior;
      CONSTRUCTOR Init(VAR Bounds  : TRect;
                       ATitle  : TTitleStr;
                       ANumber : Integer;
                       InitMortgageData :
                       MortgageDialogData);
      CONSTRUCTOR Load(VAR S : TStream);
      PROCEDURE   Store(VAR S : TStream);
      PROCEDURE   HandleEvent(Var Event : TEvent); VIRTUAL;
      PROCEDURE   ExtraPrincipal;
      PROCEDURE   PrintSummary;
      DESTRUCTOR  Done; VIRTUAL;
    END;
VAR
  HouseCalc : THouseCalcApp;  { This is the application object itself  }
  TempMtg   : PMortgageView;  { Temporary pointer for mortgage windows }
CONST
  DefaultMortgageData : MortgageDialogData =
    (PrincipalData : 100000;
     InterestData  : 10.0;
     PeriodsData   : 360);
  RMortgageView : TStreamRec =
    (ObjType : 1101;
     VMTLink : Ofs(TypeOf(TMortgageView)^);
     Load    : @TMortgageView.Load;
     Store   : @TMortgageView.Store);
  RMortgageTopInterior : TStreamRec =
    (ObjType : 1102;
     VMTLink : Ofs(TypeOf(TMortgageTopInterior)^);
     Load    : @TMortgageTopInterior.Load;
     Store   : @TMortgageTopInterior.Store);
  RMortgageBottomInterior: TStreamRec =
    (ObjType : 1103;
     VMTLink : Ofs(TypeOf(TMortgageBottomInterior)^);
     Load    : @TMortgageBottomInterior.Load;
     Store   : @TMortgageBottomInterior.Store);

PROCEDURE RegisterAllTypes;
BEGIN
  RegisterType(RScrollBar);
  RegisterType(RFrame);
  RegisterFInputLine;
  RegisterType(RMortgage);    { RMortgage defined in unit MORTGAGE.PAS }
  RegisterType(RMortgageView);
  RegisterType(RMortgageTopInterior);
  RegisterType(RMortgageBottomInterior);
END;

FUNCTION ExecDialog(P: PDialog; Data: Pointer): Word;
VAR
  Result: Word;
BEGIN
  Result := cmCancel;
  P := PDialog(Application^.ValidView(P));
  IF P <> NIL THEN
  BEGIN
    IF Data <> NIL THEN P^.SetData(Data^);
    Result := DeskTop^.ExecView(P);
    IF (Result <> cmCancel) AND (Data <> NIL) THEN P^.GetData(Data^);
    Dispose(P, Done);
  END;
  ExecDialog := Result;
END;

{------------------------------}
{   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.LoadMortgage;
VAR
  FileName : FNameStr;
  FetchMtg : PBufStream;
BEGIN
  FileName := '*.MTG';
  IF ExecDialog(New(PFileDialog, Init('*.*', 'Load Mortgage File',
    '~N~ame', fdOpenButton, 100)), @FileName) <> cmCancel
  THEN
    BEGIN
      FetchMtg := New(PBufStream,Init(FileName,stOpenRead,1024));
      IF FetchMtg^.Status <> 0 THEN Halt(1);
      TempMtg := PMortgageView(FetchMtg^.Get);
      IF FetchMtg^.Status <> 0 THEN
        BEGIN
          Writeln('Status code =',FetchMtg^.Status);
          Halt(1);
        END;
      Dispose(FetchMtg,Done);
      DisableCommands([cmSaveMortgage]);
      IF TempMtg <> NIL THEN
        Desktop^.Insert(TempMtg);
      EnableCommands([cmSaveMortgage]);
    END;
END;

PROCEDURE THouseCalcApp.SaveMortgage;
VAR
  FileName : FNameStr;
  SaveMtg  : PBufStream;
BEGIN
  FileName := '*.MTG';
  IF ExecDialog(New(PFileDialog, Init('*.*', 'Save Mortgage File',
    '~N~ame', fdOpenButton, 100)), @FileName) <> cmCancel
  THEN
    BEGIN
      SaveMtg := New(PBufStream,Init(FileName,stCreate,1024));
      IF SaveMtg^.Status <> 0 THEN Halt(1);
      IF DeskTop^.Current <> NIL THEN
        BEGIN
          SaveMtg^.Put(DeskTop^.Current);
          IF SaveMtg^.Status <> 0 THEN
            BEGIN
              Writeln('Status value =',SaveMtg^.Status);
              Halt(1);
            END;
        END;
      Dispose(SaveMtg,Done);
    END;
END;

PROCEDURE THouseCalcApp.HandleEvent(VAR Event : TEvent);
BEGIN
  TApplication.HandleEvent(Event);
  IF Event.What = evCommand THEN
    BEGIN
      CASE Event.Command OF
        cmNewMortgage  : NewMortgage;
        cmLoadMortgage : LoadMortgage;
        cmSaveMortgage : SaveMortgage;
        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('~O~pen','F3',kbF3,cmLoadMortgage,hcNoContext,
      NewItem('~S~ave Top','F2',kbF2,cmSaveMortgage,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;

CONSTRUCTOR TMortgageTopInterior.Load(VAR S : TStream);
BEGIN
  TView.Load(S);
END;

PROCEDURE TMortgageTopInterior.Store(Var S : TStream);
BEGIN
  TView.Store(S)
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   Int.  Periods  Payment',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[1],STemp,Color);         { At beginning of buffer B }
  Str(Mortgage^.Interest*100:7:2,STemp);
  MoveStr(B[10],STemp,Color);      { At position 14 of buffer B }
  Str(Mortgage^.Periods:4,STemp);
  MoveStr(B[20],STemp,Color);      { At position 25 of buffer B }
  WriteLine(0,1,Size.X,1,B);
  Str(Mortgage^.MonthlyPI:7:2,STemp);
  MoveStr(B[27],STemp,Color);      { At position 29 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;

CONSTRUCTOR TMortgageBottomInterior.Load(VAR S : TStream);
BEGIN
  TScroller.Load(S);
END;

PROCEDURE TMortgageBottomInterior.Store(Var S : TStream);
BEGIN
  TScroller.Store(S)
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
  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;

CONSTRUCTOR TMortgageView.Load(VAR S : TStream);
VAR
  MortgageTemp : PObject;
BEGIN
  TWindow.Load(S);       { Load what you've inherited from parent type}

  MortgageTemp := S.Get; { Load the contained TMortgage object }
  { Now we have to copy the heap-based copy of TMortgage to the copy }
  { embedded in the TMortgageView object we're in the process of loading: }
  Move(MortgageTemp^,Mortgage,Sizeof(TMortgage));

  GetSubViewPtr(S,BottomInterior);
  GetSubViewPtr(S,TopInterior);
  TopInterior^.Mortgage := @Mortgage;
  BottomInterior^.Mortgage := @Mortgage;
END;

PROCEDURE TMortgageView.Store(VAR S : TStream);
BEGIN
  TWindow.Store(S);   { Store what you've inherited from parent type}
  S.Put(@Mortgage);   { Store the contained TMortgage object }
  PutSubViewPtr(S,BottomInterior);
  PutSubViewPtr(S,TopInterior);
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
  RegisterAllTypes;
  HouseCalc.Init;
  HouseCalc.Run;
  HouseCalc.Done;
END.











Copyright © 1992, Dr. Dobb's Journal


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.