STRUCTURED PROGRAMMING

Stuck Windows

Jeff Duntemann KG7JF

My first house was one of those "Polish battleships," as we called them; a skinny little boxcar of a place on a lot measuring 30 feet by 180 feet, in the thick of the north side of Chicago. The guy next door (whose bedroom window was probably eight feet from ours) would set his clock radio to go off at 5:45 AM, playing early punk rock loud enough to wake a patronage worker, let alone the dead. (The dead had long since moved to Hoffman Estates, from which they returned only on election days.)

It was, in short, your classic American "starter home," meaning that almost every day, one damned thing or another about it was starting up.

Our first house, like most old houses, had openable windows and nonopenable windows. The nonopenable windows were exactly like the openable windows, except that they had 47 coats of dark green paint on them, and had not been opened since 1934.

The north kitchen window was an exception, in that it looked like it might have been opened as recently as 1958. I did the usual scraping and tugging and pulling and razorblading to no avail. And that's where it sat for quite a while, and at best I would pick at the cracks from time to time, furious at my inability to get a better view of the wall of the next house over.

My friend George Ewing was visiting one spring weekend, and he spent some time over coffee watching me thump and shove and fiddle with the just-barely-stuck kitchen window. "What's the problem, Jeff?" he finally asked.

"Anything with handles oughta open," was my disgruntled response. He shrugged, shoved me aside, and grabbed one of the window's handles in each hand. George outmasses me by a considerable fraction, and has the sort of hands you would imagine could crush rocks. He sucked in his breath and heaved upward, hard. The window popped and crackled and resisted, and then with a crumbling crunch both handles came off in his oversized hands. The window hadn't budged a fraction of an inch.

"I guess it doesn't have handles anymore," I said, and went looking for the Plastic Wood.

Opening Windows

Regardless of what happens under the surface to manage user input events, most people consider Turbo Vision to be a text-based windowing manager. The action in a Turbo Vision app happens in one or more windows. Behind the windows is just the empty pattern of the desktop.

So it's time to talk about what it takes to open and use windows in Turbo Vision. Again, I'll be using my HCALC mortgage calculator program as example code. I've submitted a new and somewhat improved version of the program as a whole to DDJ, and it can be downloaded from CompuServe and M&T Online. I won't be listing the full program here, but I will list program segments that illustrate the creation and use of Turbo Vision windows.

First, some overview and a little recap. A Turbo Vision window is a group--a collection of views whose operation is coordinated by a "boss" object of the TWindow type. The TWindow has no visible elements within itself; everything you see of a TWindow actually belongs to one of the TWindow's views. Typically, a window owns a frame, one or more interiors called panes, and often one or more scroll bars. At minimum, a window needs a frame and a pane. The frame is necessary because the top edge of the frame contains the two buttons that zoom and close the window. The pane is necessary because all Turbo Vision views are responsible for the screen space they enclose. A window cannot be a frame around nothing. The window must be able to redraw itself whenever it moves or changes size; hence it must have some sort of redrawable interior.

Window Design

I've long quibbled with the wisdom of subsetting an already-tiny 25x80-character text screen into smaller entities. (My first project published in DDJ, in fact, was a sort of "antiwindowing" system that treated the whole screen as a window into a larger, 66x80 virtual screen.) Some of my reservations remain, but in fact you can put the tininess of a screen window to good use hiding application complexity if you put some forethought into your window design. HCALC provides a good example. Its mortgage display window actually exists in two levels. Figure 1 shows the first level, which you see as soon as you instantiate a new mortgage by selecting the initial values of principal, interest, and periods from the dialog box. All you see are the three most important elements of the mortgage amortization table with a mortgage summary at the top of the window.

You can instantiate several mortgage windows with varying initial values and have them all on the screen and visible at once. This allows you to do some real-time comparisons of payments when you're trying to decide what sort of mortgage you can afford.

There is, however, an additional level a mortgage window can display. If you click on the zoom button of a mortgage window, you'll see the full-screen display shown in Figure 2. Here there are three additional columns: one for additional principal values, and two more that summarize the cumulative totals of principal and interest that hold for any given mortgage payment. This allows more detailed analysis of how many payments you can lop off the end of a mortgage by remitting extra principal during the mortgage's course--and reminds you how much money goes down the interest rathole compared to principal. (HCALC has been a wonderful goad toward paying off our mortgage early!)

The interior layout of the window was designed to display only the essential elements of a mortgage amortization table when in a "normal" (that is, non-zoomed) state, and only display the full information table when the window is zoomed. This was done mostly by choosing an initial window size to place the three extra columns outside the right window margin.

So work smart when you put your windows together. Windows are windows primarily so that you can display more than one on the screen at once. Think through the question of what use it might be to display multiple windows at once--and design the display of information within the window to support whatever comparison would be useful.

Defining Windows

Putting Turbo Vision windows together is complicated by the fact that you don't define everything within a single object definition. You have to define a TWindow descendent to "be" the window--but you have to define one or more interior objects separately and then use the Insert method to insert them into the window object. (Windows, remember, are groups, and you have to insert a window's views into the window.)

Listing One (page 145) contains the definitions for three classes: TMortgageView, the mortgage window itself (a group) and the two panes: TMortgageTopInterior and TMortgageBottomInterior, both of which are views. Note that there is nothing explicit in the TMortgageView definition to connect it to either of the pane objects. That connection is done strictly at runtime, through the Insert method.

This, however, causes a problem. Both of the panes need to display information contained in the mortgage class, TMortgage, present in the TMortgageView object that owns the panes. The panes do not descend from TMortgageView, so they do not have access to TMortgageView's fields. It would be wasteful to give each pane its own mortgage object, so what we do is provide a pointer in each pane to point to the mortgage object contained in TMortgageView. When the mortgage window is instantiated, the two Mortgage pointer's in the two panes must be set to point to the Mortgage field in TMortgageView. This is fast and memory efficient, but lordy, something about it still makes me wince.

The lesson learned here is something to put in your notebook: The fields of a group object are not automatically available to the objects owned by the group!

Drawing Windows

At minimum, the panes of a window need to have two methods: a constructor, which sets up the pane, and a Draw method, which draws the pane to the screen on demand. A destructor is optional; the default destructor is inherited from the pane's parent class and for simple panes serves quite well.

The Draw method of a view is called whenever the view changes size or moves. It must draw every portion of the rectangle occupied by the view. Generally, you the programmer don't have to call a view's Draw method directly. Turbo Vision knows when a view needs to be redrawn, and it will call Draw for you--assuming you set the Draw method up correctly to begin with.

The TMortgageTopInterior.Draw method is fairly simple, and is a good example of how a view must draw the space it owns. Note that the drawing is not done with Write or Writeln! What you have to do is declare a buffer (of type TDrawBuffer), fill the buffer with text using the MoveStr library routine, and then use one of TView's methods, WriteLine, to actually write the buffer to the screen.

In between uses, you must clear the buffer variable to spaces using the MoveChar routine. If you wish, you can use some other character (one of the "halftone" characters, perhaps) to write text against a slightly fancier background.

Again, this whole process of writing text to a view's interior seems needlessly prolix to me, and I would like to see it simplified in some future adaptation of Turbo Vision.

Scrollers and Scroll Bars

The bottom pane is a little different and a little more complex. The top pane only serves to summarize the mortgage and (as a side benefit) provides some column headers for the mortgage amortization table. The bottom pane does something a lot tougher, but much more characteristic of a window: It has to display some subset of a much larger block of data, not all of which can fit in the window at once.

Turbo Vision contains most of the machinery to do this, in the form of a special TView descendent class called TScroller. Generically, we'll call objects of type TScroller simply "scrollers."

Functionally, scrollers are windows into a larger block of text. A scroller scrolls through the larger block of text in either X (across) or Y (up and down) or both, as needed. It's possible to scroll in only one dimension if the data fits entirely within the scroller in the other dimension.

In order to work, you have to pass one or two scroll bars to the scroller when you call its constructor. Turbo Vision scroll bars are "finished" views, and you don't need to specialize them by subclassing them. In fact, if your scroll bar is typical and extends across one whole side of the window (the right side for a vertical scroll bar and the bottom for a horizontal scroll bar) you can automatically set the sizing parameters to the scroll bars by calling a Turbo Vision library routine called StandardScrollBar, as I've done in TMortgageView.Init.

But note that I've only called StandardScrollBar for the horizontal scroll bar. The vertical scroll bar isn't quite typical, in that it only embraces the bottom pane of the mortgage window, and not the full vertical height of the window. To play tricks like that you have to set the scroll bar's Origin and Size records explicitly. I've done this for the vertical scroll bar in TMortgageView.Init, as you can see in Listing Two (page 145).

Using Scroll Bars

Scroll bars are one of the nifty-neato aspects of Turbo Vision, and they definitely illustrate the advantages of event-driven programming. The scroll bars are highly independent and self-contained and don't require a lot of fooling-with. The user sets the state of a scroll bar by pushing the slider character with the mouse, or else using the arrow keys in the keypad. (PgUp and PgDn also work with the vertical scroll bar.) All of that is done beneath the surface, below the level of your application. All you have to do is build the state of the scroll bar into your algorithm for redrawing the pane.

When you initialize a scroller with scroll bars, you must call TScroller.SetLimit, which sets the maximum value of "travel" that each scroll bar may work through. (This is done in HCALC near the bottom of TMortgageView.Init.) In HCALC, the maximum horizontal travel is 80 (the width of the mortgage amortization table) and the maximum vertical travel is the number of periods in the mortgage being displayed by the window. The default mortgage is the 30-year mortgage, which has 360 entries; hence the default maximum travel in Y is 360. These maximum travel values are set in the scroller's Limit record by SetLimit.

The state of the scroll bars is read through a single record called Delta, contained in the TScroller object that owns the scroll bars. The value of Delta.X is proportional to the position of the slider character within the scroll bar (running from 0 to Limit.X), as it exists at the time you read Delta.X. (Turbo Vision updates the values in Delta automatically.) Similarly, the value of Delta.Y is proportional to the position of the slider character within the scroll bar, running from 0 to Limit.Y. For example, if Limit.Y is 360 (the default), Delta.Y will be at 180 when the vertical scroll bar's slider character is exactly halfway through its travel.

To make the contents of a scroller reflect the position of its scroll bars, you have to take the Delta values into account in your Draw method. Read TMortgageBottomInterior.Draw carefully, and you'll see how it's done. Vertical scrolling is handled by using Delta.Y as part of the index value that selects which elements of the mortgage amortization table array are displayed to the screen. Horizontal scrolling is even easier: You simply use Delta.X to select the starting point within the buffer that you display to the screen: WriteLine(O, YRun, Size.X,1,B[Delta.X]); where B contains the string data that must be displayed to the screen. B contains the full width of the amortization table--all 80 characters' worth. If Delta.X is 0, you start the display at the beginning of the buffer. If Delta.X is 40, you start the display with the middle of the buffer--and hence half the horizontal display will appear to have scrolled out of the window to the left.

Growing Panes

Take a look at the two constructors for the top and bottom panes of the mortgage window. You'll see a couple of low-key statements that make a lot of difference in how a window looks and acts. Look first at MortgageBottomInterior.Init. Note the statement that sets the "grow mode": GrowMode: = gfGrowHiX + gfGrowHiY;. The two gf constants dictate which way the window can expand and contract. This statement says that the window can be expanded in both X and Y by tugging on the lower-right corner. The bottom pane is a scroller, and is really a window onto something larger than the window can initially display in both dimensions (that is, the mortgage amortization table, which is typically 360 lines long and has some "extra" display fields normally hidden beyond the right margin).

Now look at MortgageTopInterior.Init. It sets its grow mode like this:GrowMode:= gfGrowHiX;. This statement allows the top pane of the mortgage window to grow in X (that is, to the right) only. You cannot increase or decrease the vertical height of the top pane. This makes sense--the top pane really just summarizes the mortgage, and as a side-service provides column headers for the display of the amortization table in the lower pane. There's nothing more to see in the Y dimension that isn't already shown--so there's no need to grow it. Furthermore, closing up the top pane in Y wouldn't gain you more than a few lines, and would greatly obscure the meaning of the window as a whole. Therefore, you can't shrink the pane, either.

Because the top pane contains some additional header information to the right of the right margin, it makes sense to grow the top pane in X, hence the gfGrowHiX constant.

I recommend messing around with the GrowMode statements in both panes. Allow the top pane to grow in both X and Y and see what happens when you pull on the corner of the window. Comment out the GrowMode statement in the top pane's constructor and see what happens when you try to grow the top pane in X.

Framed!

Although we usually only think of the window as a whole being framed, individual panes can, in fact, have frames. The bottom pane of the mortgage window has one, and its frame is the source of the line that divides the top from the bottom pane. If you comment out the following line in MortgageBottomInterior.Init, the frame will vanish: Options:= Options OR Framed;. It's a cosmetic touch, true, but it adds a lot to the readability of the data in the window. If you add a frame to the top pane, nothing much will happen. However, if you give the top pane a frame (by ORing the Framed constant with its Options value) and then comment out the top pane's GrowMode statement, you'll see the right edge of the top pane's frame when you try to grow the window in X. Furthermore, you won't see the additional column titles to the right of the frame. The frame marks the edge of the pane--but you've grown the window in X, beyond the now-fixed extent of the pane.

Don't Paint 'Em Shut!

That's the quick tour of creating and using windows under Turbo Vision. The subject is a lot deeper than that, but much of the rest is refinement. If you fully grasp the material I've covered in this column and in HCALC.PAS, you should be able to open some reasonably workable (if not excessively fancy) windows. The key point is understanding what you're doing, and what's going on inside Turbo Vision. Running blind and lifting Turbo Vision code from other people without knowing how it works is the software equivalent of painting your windows shut. You're fine until you want to change the view, as it were--and then, like me back on Campbell Street in Chicago, you'll just be...stuck.

And if it seemed like a hairy business, well, shove your cowboy hats on tight, buckaroos, because next time we have to rope and hawgtie Turbo Vision streams. Yippee-I/O-ki-yikes!

Q & A

Whew. Another Halloween. Three years here in DDJ, revelling in this fine madness. It seems like maybe an hour and a half, even considering that that was two houses, a state, an earthquake, the crumbling of the Communist Threat, and two major releases of Turbo Pascal ago.

I want to thank you for the mail. You've taught me a lot, especially about Zeller's Congruence and the freakiness of the PC serial port. And in closing out another year (with no end in sight) of "Structured Programming," I'd like to share a handful of the comments I've received that have had nothing whatsoever to do with programming at all. For example, someone asked if am I consciously imitating Dave Barry. The answer is yes. (I am not making this up!)

Or: Why is the Magic Van magic? Easy: Because it's gone over 90,000 miles and I still have it.

Mr. Byte remains a popular topic: "The next time Mr. Byte has puppies, we'd be interested." So would Ripley. But hey, I know what you mean. Trouble is, Mr. Byte is a factory second (pink nose, horrors!) and he had to surrender the family jewels before we could get clear title to him. Check the Bichon Frise section of the breeder directory in Dog World. Don't buy puppies from pet stores!

Not everybody wants puppies: "Stick to business. I don't care what Mr. Byte urinates on."

Ahhh. You must have cats.



_STRUCTURED PROGRAMMING COLUMN_
by Jeff Duntemann


[LISTING ONE]


{---------------------------------}
{   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;




[LISTING TWO]


  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;




[LISTING THREE]


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.




[LISTING FOUR]


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 © 1992, Dr. Dobb's Journal