_STRUCTURED PROGRAMMING COLUMN_ by Jeff Duntemann 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.