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.
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.
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.
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!
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.
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).
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.
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.
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.
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!
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.
Copyright © 1992, Dr. Dobb's JournalOpening Windows
Window Design
Defining Windows
Drawing Windows
Scrollers and Scroll Bars
Using Scroll Bars
Growing Panes
Framed!
Don't Paint 'Em Shut!
Q & A
_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.