Channels ▼
RSS

Tools

Extending Turbo Vision

Source Code Accompanies This Article. Download It Now.


NOV92: EXTENDING TURBO VISION

Scott works as a consultant in the Philadelphia area and specializes in networks, communications, and RDBMS. You can reach him on CompuServe at 72611,2511.


Borland's application framework, Turbo Vision, is based upon the event-driven paradigm and has a particularly elegant application in the object-oriented model. Objects derived from the TView base class receive notification of external events as messages. But because of its object-oriented and event-driven nature, programmers moving from traditional environments to Turbo Vision must rethink some of the most fundamental techniques they apply.

This article presents a basic programming scenario that must be reworked in Turbo Vision and explores Turbo Vision's event-generation and event-handling methodologies. I'll also show how to extend the framework by adding an event based on the BIOS timer-tick counter that replaces the TApplication .Idle method.

Traditional Programming

Figure 1 provides an example of a traditional programming method that must be retooled in an event-driven environment. I've used the GetKey function in Figure 1 in almost every program I've written for DOS. Its purpose is to get user input from the keyboard while using the idle time, during which there is no keypress to perform other work. In this case, the function updates the time-of-day display on the screen. GetKey contains a loop which polls for the occurrence of one or more events. As written, such a loop should not be used in an event-driven environment, because it is the environment's job to do the polling for exterior events and notify the program when they occur. (I emphasize should, because Turbo Vision allows the recalcitrant programmer to call GetEvent directly, thus circumventing the event-driven framework.)

Figure 1: GetKey is a typical example of traditional programming methods that must be retooled in an event-driven environment

  function GetKey: Integer;
  var
    Ch: Char;
  begin
    while not KeyPressed do
      UpdateScreenTime;
    Ch := ReadKey;
    if Ch = #0 then
      GetKey := ord (ReadKey) or $80;
    else
      GetKey := ord (Ch);
  end;

The Event-driven Approach

Given that I cannot use the code in Figure 1 in an event-driven environment, how do I achieve the same result; that is, how do I maintain a current time display? The Windows environment provides applications with a set of timers that can be used to generate a periodic message for either the application itself or a particular window within it. The procedure to update the time can be invoked whenever a timer message is received from Windows. Turbo Vision provides a natural, but slightly less elegant means by which the application can handle such periodic tasks.

The Turbo Vision processes of event generation and handling are encapsulated in the TGroup.Execute method. TApplication.Run, for example, simply invokes the Execute method. Other TGroup descendants such as TDialog will run Execute whenever ExecView is called for them. The "current modal view" is the TView whose Execute method was last entered. Figure 2 is a simplified representation of how the Execute method operates. The exact functionality of the GetEvent and HandleEvent methods will vary, depending on the nature of the TGroup descendant that is executing. The HandleEvent method is where most of the program functionality is actually hooked into the framework. The GetEvent method, on the other hand, is more of a black box, as it is seldom overridden. TView.GetEvent simply calls Owner^.GetEvent. Since virtually no descendant of TView overrides this, most, if not all GetEvent calls eventually chain back to the TApplication view. In other words, it is almost always safe to think of any GetEvent method as synonymous with Application^.GetEvent.

Figure 2: Representation of the Execute method operation.

  EndState := 0;
  repeat
    GetEvent (Event);
    HandleEvent (Event);
  until EndState <> 0:

Why not Idle?

Figure 3 is a simplified representation of TApplication.GetEvent and refers to the TApplication.Idle method, which gives the programmer a hook into the idle time between external events. The TVDEMO program provided by Borland, for example, overrides the Idle method to update displays of the current time-of-day and heap remaining.

Figure 3: Representation of TApplication.GetEvent method operation.

  GetMouseEvent (Event);
  if Event.What = evNothing then begin
    GetKeyboardEvent (Event);
    if Event.What = evNothing then
      Idle;
  end;

There are cases in which you might not wish to use the TApplication.Idle method and therefore must create a new methodology for performing periodic tasks. One reason for not using the Idle method is that Windows has no analog, so in order to port Turbo Vision code to Windows, a different methodology must be used to perform idle tasks. The second reason may be more compelling, especially to object-oriented purists. In order to exploit the Idle method, TApplication must have knowledge of all the views that want to process during the idle time, whether those views are subviews of TApplication or of a TGroup several groups removed from it in the view chain. This violates the broad sense of the concept of encapsulation. A far more elegant approach is to allow views to hook into idle processing via their own HandleEvent methods.

Extending Turbo Vision

It's important to realize that with Turbo Vision, it is code within the application itself that supports the event-driven paradigm, rather than an operating environment external to the program. Because of this, it is possible to extend the process in ways that Windows, for example, cannot support at the application level. In order to abate both of my Idle method objections, I have extended TApplication.GetEvent to add a new timer event analogous to, but not completely compatible with that of Windows.

Listing One (page 198) is a small unit to support the new event. The GetBiosTickEvent procedure will generate an event whenever it finds that the BIOS timer-tick counter has changed since the last call to the procedure. While this will nominally produce an event every 55 milliseconds, this frequency need not be very accurate; entire ticks may be lost if the procedure is not called often enough. For this reason, the BIOS tick event uses the Event.InfoLong field to carry the BIOS tick count at the moment the event was generated. The unit also provides a GetBiosTicks function so that the current tick count can be obtained at any point in the program.

I've included a demo program with this article called TVTIME.PAS; see "Availability" on page 5. To actually generate the new events, I override the application's GetEvent method. In TVTIME, the new GetEvent method calls TApplication.GetEvent to check for mouse and keyboard events. If no such events are found, Event. What is evNothing and I check for a BIOS tick event. If there is still no event, GetEvent generates a new Idle event. While the application described shortly does not actually use this, I have included the Idle event for those programs that may have good use for the idle time or for which the 55-millisecond clock resolution is too coarse for good performance. Since timer ticks are polled after the mouse and keyboard, they have a lower priority. This priority can be reversed simply by reversing the order of the calls; I chose lower priority to parallel the Windows scheme.

Event Handling and Modality

The GetEvent method in TVTIME is not quite as simple as described in the preceding paragraph, due to the effect modality has on event handling. You will recall from the description of the TGroup.Execute method that the current modal view is in its Execute method. The call to GetEvent from this method eventually propagates to our new application GetEvent so that our new events can be generated even when the current modal view is not TApplication. HandleEvent, on the other hand, is not as cooperative. For the typical view, this method does some specialized handling, such as TDialog handling a cmCancel command, then calls the event handlers of each of its subviews. When TApplication is the current modal view, all views in the application will have a chance to handle the event. However, any other current modal view will hog events for itself and it subviews.

However, I want the timer event to be available to all views at all times. Otherwise, my display clock, for example, will stop ticking every time I open a modal dialog box. For this reason, I defined a new event class evMetaBroadcast that will be sent to all views, regardless of the current modal view. To support the metabroadcast concept, GetEvent checks whether the application is the current modal view, a pointer to which is available from the TView.Top-View method. If the GetEvent call is from another modal view, TApplication.GetEvent handles metabroadcast events itself by directly calling TApplication.HandleEvent. Doing so allows all views in the application to receive the event.

The Example Application

The TVTIME demo is trivial by Turbo Vision standards. It has one menu, an About dialog box, and a few hot spots on the status bar. Its main purpose is to demonstrate two of the objects in Listing Two (page 198). These are variants of the clock and heap display objects from the GADGETS unit used by TVDEMO. Rather than rely on the application to tell them to update during idle processing, these update themselves when triggered by the timer event.

Both TClockView and THeapView are descendants of TTickView, which provides the skeleton for a view that is updated at each timer-tick event. An essential feature of the TTickView is that its constructor sets the evMetaBroadcast bit in its EventMask flag. This allows the parent TGroup to pass this event to the view.

Note that it is unlikely you would ever want to do a ClearEvent after handling an evMetaBroadcast. The idea is that every view gets notified of an event of this class. Also note that I have made TTickView a true abstract object by calling the Turbo Pascal Abstract procedure from methods that must be overridden. If they are not, a runtime error is generated.

The program allows you to disable either or both the clock and/or heap displays. It also allows you to disable support for metabroadcasting. By doing this, you can see the effect of modality. When TApplication is not the current modal view, the clock stops. I was interested to discover in this way that the menu bar is itself a modal view.

Conclusion

I have used the timer tick and idle events in other programs to support serial-input polling. They could also be used to perform lengthy calculations in the background while still allowing the user to abort at any time. You can also extend what I've presented here to add still more events to your Turbo Vision environment. For example, you can use the alarm capabilities of the 146818A real-time clock chip, directly or via BIOS interrupt 1AH, to generate alarm events. Or you could add a GetSerialEvent procedure to directly generate serial-data input events rather than polling for input during timer tick or idle events.

Bibliography

Duntemann, Jeff. "Stuck Windows." DDJ (February, 1992).

Duntemann, Jeff. "Chewing the Wrapper." DDJ (January, 1992).

Duntemann, Jeff. "The Tragedy of the Black Box." DDJ (December, 1991).

Duntemann, Jeff. "Waves in What?." DDJ (November, 1991).

Roach, Kenneth. "Using the Real-Time Clock." DDJ (June, 1991)

Frid-Nielsen, Lars and Alex Lane. "Celestial Programming With Turbo Pascal." DDJ (June, 1991).



_EXTENDING TURBO VISION_
by Scott Nichol


[LISTING ONE]
<a name="02aa_0010">


{***********************************************************************}
{   BIOSTICK.PAS                                                        }
{                                                                       }
{   Support for BIOS tick counter.  The new BIOS tick event is of class }
{   evMetaBroadcast, command cmBiosTick.  The Event.InfoLong field      }
{   contains the tick counter value at the time of the event.  The      }
{   current value can be obtained using the GetBiosTicks function.      }
{   Because this event is generated on a cooperative rather than        }
{   preemptive basis, there may not be an event generated for every     }
{   tick of the counter.  Nor should any assumptions be made about the  }
{   accuracy of the periodicity of the event: the nominal periodicity   }
{   of 55 milliseconds will only be obtained when no other events are   }
{   generated and cmBiosTick handling takes under 55 milliseconds.      }
{***********************************************************************}

{$R-,S-}

unit
  BiosTick;

interface

uses
  Drivers;

procedure GetBiosTickEvent(var Event: TEvent);
function GetBiosTicks: LongInt;

implementation

uses
  Cmds;

var
  BiosTicks: LongInt absolute $40:$6c;

procedure GetBiosTickEvent(var Event: TEvent);
const
  OldTicks: LongInt = 0;
begin
  if BiosTicks <> OldTicks then begin
    OldTicks := BiosTicks;
    with Event do begin
      What := evMetaBroadcast;
      Command := cmBiosTick;
      InfoLong := OldTicks;
    end;
  end else
    Event.What := evNothing;
end;

function GetBiosTicks: LongInt;
begin
  GetBiosTicks := BiosTicks;
end;

end.


<a name="02aa_0011">
<a name="02aa_0012">
[LISTING TWO]
<a name="02aa_0012">

{***********************************************************************}
{   TICKVIEW.PAS                                                        }
{                                                                       }
{   Views to be driven by cmBiosTick.  The heap and clock views were    }
{   inspired by the Gadgets unit provided by Borland in the TVDEMOS     }
{   subdirectory of Turbo Pascal 6.0.                                   }
{***********************************************************************}

unit TickView;

{$R-,S-,V-}

interface

uses
  Drivers, Objects, Views, App;

type
  PTickView = ^TTickView;
  TTickView = object(TView)
    Display: Boolean;
    constructor Init(var Bounds: TRect);
    procedure Draw; virtual;
    procedure HandleEvent(var Event: TEvent); virtual;
    function DoDraw: Boolean; virtual;
    procedure DrawInfo(var S: String); virtual;
    procedure ToggleDisplay; virtual;
  end;

  PHeapView = ^THeapView;
  THeapView = object(TTickView)
    OldMem: LongInt;
    constructor Init(var Bounds: TRect);
    function DoDraw: Boolean; virtual;
    procedure DrawInfo(var S: String); virtual;
  end;

  PClockView = ^TClockView;
  TClockView = object(TTickView)
    OldTime: LongInt;
    TimeStr: String[8];
    constructor Init(var Bounds: TRect);
    function DoDraw: Boolean; virtual;
    procedure DrawInfo(var S: String); virtual;
  end;

implementation

uses
  Dos,
  BiosTick, Cmds;

{------ TTickView (abstract) ------}

constructor TTickView.Init(var Bounds: TRect);
begin
  TView.Init(Bounds);
  EventMask := EventMask or evMetaBroadcast;
  Display := True;
end;

procedure TTickView.Draw;
var
  S: String;
  B: TDrawBuffer;
  C: Byte;
begin
  C := GetColor(2);
  MoveChar(B, ' ', C, Size.X);
  DrawInfo(S);
  if Display then
    MoveStr(B, S, C);
  WriteLine(0, 0, Size.X, 1, B);
end;

procedure TTickView.HandleEvent(var Event: TEvent);
begin
  TView.HandleEvent(Event);
  if Event.What = evMetaBroadcast then
    case Event.Command of
    cmBiosTick:
      if DoDraw then DrawView;
    end;
end;

function TTickView.DoDraw: Boolean;
begin
  Abstract;
end;

procedure TTickView.DrawInfo(var S: String);
begin
  Abstract;
end;

procedure TTickView.ToggleDisplay;
begin
  Display := not Display;
  DrawView;
end;

{----------- THeapView ------------}

constructor THeapView.Init(var Bounds: TRect);
begin
  TTickView.Init(Bounds);
  OldMem := 0;
end;

function THeapView.DoDraw: Boolean;
begin
  DoDraw := OldMem <> MemAvail;
end;

procedure THeapView.DrawInfo(var S: String);
begin
  OldMem := MemAvail;
  Str(OldMem: Size.X, S);
end;

{---------- TClockView ------------}

constructor TClockView.Init(var Bounds: TRect);
begin
  TTickView.Init(Bounds);
  OldTime := 0;
end;

function TClockView.DoDraw: Boolean;
begin
  DoDraw := (GetBiosTicks - OldTime) >= 18;
end;

procedure TClockView.DrawInfo(var S: String);
var
  Hour, Minute, Second, Sec100: Word;
  Param: record
    Hr, Min, Sec: LongInt;
  end;
begin
  OldTime := GetBiosTicks;
  GetTime(Hour, Minute, Second, Sec100);
  with Param do begin
    Hr := Hour;
    Min := Minute;
    Sec := Second;
  end;
  FormatStr(S, '%02d:%02d:%02d', Param);
end;

end.





<a name="02aa_0013">
[LISTING ONE - EXTRA]
<a name="02aa_0013">

{***********************************************************************}
{   TVTIME.PAS                                                          }
{                                                                       }
{   A short program to demonstrate the addition of a new TV event class }
{   that can be broadcast outside of the event chain focus.  It uses a  }
{   specific command based on the BIOS timer tick counter.              }
{                                                                       }
{   Copyright (c) 1992 Charles Scott Nichol.  All rights reserved.      }
{***********************************************************************}

{$R-,S-,X+}

program
  TVTime;

uses
  App, Dialogs, Drivers, Menus, MsgBox, Objects, Views,
  BiosTick, Cmds, TickView;

type
  TTimeApp = object(TApplication)
    MetaSupport: Boolean;
    Clock: PClockView;
    Heap: PHeapView;
    constructor Init;
    procedure GetEvent(var Event: TEvent); virtual;
    procedure HandleEvent(var Event: TEvent); virtual;
    procedure InitDeskTop; virtual;
    procedure InitMenuBar; virtual;
    procedure InitStatusLine; virtual;
    procedure OutOfMemory; virtual;
  end;

const
  cmAbout = 100;
  cmToggleClock = 101;
  cmToggleHeap = 102;
  cmToggleMeta = 103;

{----------- TTimeApp ------------}

constructor TTimeApp.Init;
var
  R: TRect;
begin
  TApplication.Init;

  MetaSupport := True;

  GetExtent(R);
  R.A.X := R.B.X - 8; R.B.Y := R.A.Y + 1;  {End of top line}
  Clock := New(PClockView, Init(R));
  if ValidView(Clock) = nil then
    Fail;
  Insert(Clock);

  GetExtent(R);
  R.A.X := R.B.X - 8; R.A.Y := R.B.Y - 1;  {End of bottom line}
  Heap := New(PHeapView, Init(R));
  if ValidView(Heap) = nil then begin
    Dispose(Clock);
    Fail;
  end;
  Insert(Heap);
end;

procedure TTimeApp.GetEvent(var Event: TEvent);
begin
  TApplication.GetEvent(Event);
  if Event.What = evNothing then begin
    GetBiosTickEvent(Event);               {Hook to add the BIOS tick event}
    if Event.What = evNothing then begin
      Event.What := evMetaBroadcast;
      Event.Command := cmIdle;             {Alternative to .Idle method}
    end;
    if MetaSupport and (Event.What = evMetaBroadcast) then begin
      if TopView <> @Self then begin       {We are not the current modal view}
        HandleEvent(Event);                {Force meta broadcast of event}
        ClearEvent(Event);                 {Prevent redundant processing}
      end;

    end;
  end;
end;

procedure TTimeApp.HandleEvent(var Event: TEvent);

  procedure About;
  const
    S1 = #3'Bios Tick Time/Heap Display Demo';
    S2 = #13#3'Copyright (c) 1992 Charles Scott Nichol';
    S3 = #13#3'All rights reserved';
    S4 = #13#3'Meta support is ';
  var
    D: PDialog;
    R: TRect;
    S5: String[15];
  begin
    R.Assign(0,0,49,10);
    D := New(PDialog, Init(R, 'About'));
    if MetaSupport then
      S5 := 'enabled'
    else
      S5 := 'disabled';
    with D^ do begin
      Options := Options or ofCentered;
      R.Assign(3, 2, Size.X - 2, Size.Y - 4);
      Insert(New(PStaticText, Init(R, S1+S2+S3+S4+S5)));
      R.Assign(19, 7, 29, 9);
      Insert(New(PButton, Init(R, 'O~k~', cmOK, bfDefault)));
      SelectNext(False);
    end;
    if ValidView(D) <> nil then begin
      DeskTop^.ExecView(D);
      Dispose(D, Done);
    end;
  end;

  procedure ToggleMeta;
  begin
    MetaSupport := not MetaSupport;
  end;

begin
  TApplication.HandleEvent(Event);
  if Event.What = evCommand then begin
    case Event.Command of
    cmAbout:
      About;
    cmToggleClock:
      Clock^.ToggleDisplay;
    cmToggleHeap:
      Heap^.ToggleDisplay;
    cmToggleMeta:
      ToggleMeta;
    end;
    ClearEvent(Event);
  end;
end;

procedure TTimeApp.InitDeskTop;
var
  R: TRect;
begin
  GetExtent(R);
  R.Grow(0,-1);            {Leave room for menu bar and status line}
  DeskTop := New(PDeskTop, Init(R));
end;

procedure TTimeApp.InitMenuBar;
var
  R: TRect;
begin
  GetExtent(R);
  R.B.Y := R.A.Y + 1;      {Top line only}
  MenuBar := New(PMenuBar, Init(R, NewMenu(
    NewSubMenu('~'#240'~', hcNoContext, NewMenu(
      NewItem('~A~bout', '', kbNoKey, cmAbout, hcNoContext,
      NewItem('Toggle ~C~lock Display', '', kbNoKey, cmToggleClock, hcNoContext,
      NewItem('Toggle ~H~eap Display', '', kbNoKey, cmToggleHeap, hcNoContext,
      NewLine(
      NewItem('E~x~it', '', kbNoKey, cmQuit, hcNoContext, nil)))))),
    nil))));
end;

procedure TTimeApp.InitStatusLine;
var
  R: TRect;
begin
  GetExtent(R);
  R.A.Y := R.B.Y - 1;      {Bottom line only}
  StatusLine := New(PStatusLine, Init(R,
    NewStatusDef(0, $FFFF,
      NewStatusKey('~Alt-X~ Exit', kbAltX, cmQuit,
      NewStatusKey('~Alt-M~ Toggle Meta Support', kbAltM, cmToggleMeta,
      NewStatusKey('~F10~ Menu', kbF10, cmMenu, nil))),
    nil)));
end;

procedure TTimeApp.OutOfMemory;
begin
  MessageBox(#3'Insufficient memory to complete operation', nil,
    mfError + mfOkButton);
end;

{----------- Program ------------}

var
  TimeApp: TTimeApp;
begin
  if TimeApp.Init then begin
    TimeApp.Run;
    TimeApp.Done;
  end;
end.


<a name="02aa_0014">
[LISTING TWO - EXTRA]
<a name="02aa_0014">

{***********************************************************************}
{   CMDS.PAS                                                            }
{                                                                       }
{   Constants for event and commands added.                             }
{                                                                       }
{   Copyright (c) 1992 Charles Scott Nichol.  All rights reserved.      }
{***********************************************************************}

unit
  Cmds;

interface

const
  evMetaBroadcast = $400;  {Use an unallocated bit from Event.What}

const
  cmBiosTick = 1000;       {These commands are for evMetaBroadcast}
  cmIdle = 1001;

implementation

end.










Copyright © 1992, Dr. Dobb's Journal


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

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

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

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

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

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

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

Video