Turbo Vision Tutorial

Part 1 -- Status Line and Menu Bar




Welcome

Hi! Welcome to the first part of Turbo Vision (TV) tutorial. Albeit already old, TV still is a quick solution on writing DOS-based applications. It offers a lot of flexibility and features. Be warned that TV-based programs are typically memory-hungry. You can set your TP7 or BP7 to Protected mode to solve this. However, this solution is limited to 16 MB. I think that this should be enough for all of us.

I should've written this tutorial 3 or 4 years ago since there were many of TV fans. Now, I just provide this to complete the whole series and for the hobbyist. I hope that you now have a firm grasp on OOP concepts as I would use a lot of them. If you are not pretty well on OOP, please re-read the four series on OOP, starting here. I adopt Borland Pascal 7.0's manual in Turbo Vision to create this tutorial. I already forgot a lot of these. So, the chance is that I made a lot of mistakes here. If you found errors or mistakes, please let me know. Thanks in advance.

Oh, I almost forgot! You need to enable the extended syntax in using TV. You can do it by either invoking Option menu or just add {$X+} thing at the very top of your program.

The Basics

All of TV-based applications are inherited from TApplication class. So, if you see the simplest TV program, it would be roughly look something like this:

uses App;

type
   TMyApp = object(TApplication)
            end;

var MyApp : TMyApp;

begin
  MyApp.Init;
  MyApp.Run;
  MyApp.Done;
end.

Wow, simple! Let's run it and see how is it look like. Then, to extend your application, you can simply inherit the methods in TApplication. That's how it works.

 

Status Bar

To extend our application above, let's modify TMyApp class above. One of the simplest (for this tutorial) is to extend the method InitStatusLine. This method is a virtual method in TMyApplication. So, the declaration will look like:

type
   TMyApp = object(TApplication)
               procedure InitStatusLine; virtual;
               function GetStatusKey(Next: PStatusItem): PStatusItem;
            end;

Hmm, that's easy. Hey, what's GetStatusKey for? Well, it is to get the key bindings for commands. Don't worry, it will be described later.

The contents of InitStatusLine is as follows:

procedure TMyApp.InitStatusLine;
var R : TRect;
begin
  GetExtent(R);
  R.A.Y := R.B.Y - 1;
  new(StatusLine, Init(R,
      NewStatusDef(0, $EFFF, GetStatusKey(nil),
      NewStatusDef($F000, $FFFF, GetStatusKey(nil),nil))));
end;

Status bars typically just one line big. So, after knowing the status bar extent (i.e. dimension), we reserve one line by subtracting the lower Y. Then we make the status line and invoke its constructor. The NewStatusDef declarations are to reserve which help context to which key binding. So, the first NewStatusDef associate the help code from 0 to $EFFF to the key in our function. The second is from $F000 to $FFFF with the same key binding. Basically, we can say only one NewStatusDef which covers 0 to $FFFF, but this example shows you that you can do it more than one with different key bindings. The usage of this separation is to display different hotkeys whenever certain help context is displayed. Of course we will make those two differ in a short moment.

If you noticed on how the big third line starting from new(..., it is arranged like a linked list which the last parameter is the next field. Yes, you're right. TV uses this trick a lot, especially in defining menu bars. So, the Init takes two argument: R and the next field. Then, the first NewStatusDef fills as the next. NewStatusDef's last argument is also the next field, which subsequently be filled by the second NewStatusDef. The second NewStatusDef's last field is the next field, which is filled with nil.

OK, now, what is GetStatusKey about? See, that the last (and the only) parameter also uses this trick. Since commands are defined as integers, it would be much more convenient to define constants correspond to it as it will increase program readability. So, before defining GetStatusKey, we define these constants:

const
  cmOrderNew    = 200;
  cmOrderWin    = 201;
  cmOrderSave   = 202;
  cmOrderCancel = 203;
  cmOrderNext   = 204;
  cmOrderPrev   = 205;
  cmClipShow    = 210;
  cmAbout       = 220;
  cmFindOrderWindow = 1002;
  cmOptionsVideo = 1502;
  cmOptionsSave  = 1503;
  cmOptionsLoad  = 1504;

How do you know these magic numbers? Well, you can assign any word constants to any commands as long as they are unique and you consistently use them as defined. Of course that TV reserves some numbers (I believe it is from 0 to 99 and 256 to 999), so you can't use them. Command number 0 to 255 can be disabled (i.e. grayed in the menu) and the rest (256 to 65,536) can not. TV also have some ready-made constants such as cmQuit, cmOpen, and cmClose. Those constants are commonly used ones. You can use them directly.

Oh, about the cm... prefix, I just try to mimic the ones used in TV to make them more "uniform". You can name them differently if you please.

procedure TMyApp.GetStatusKey(Next: PStatusItem): PStatusItem;
begin
  GetStatusKey := NewStatusKey('', kbAltX,   cmQuit,
                  NewStatusKey('', kbF10,    cmMenu,
                  NewStatusKey('', kbAltF3,  cmClose,
                  NewStatusKey('', kbF5,     cmZoom,
                  NewStatusKey('', kbCtrlF5, cmResize,
                  NewStatusKey('', kbF6,     cmNext,   Next))))));
end;

Again, you can quickly notice the kb... constants. They are already defined in TV. It's somehow intuitive and easy to remember.

Let's reexamine our previous InitStatusLine and we make it a bit different now:

procedure TMyApp.InitStatusLine;
var R : TRect;
begin
  GetExtent(R);
  R.A.Y := R.B.Y - 1;
  new(StatusLine, Init(R,
      NewStatusDef(0, $EFFF,
          NewStatusKey('~F3~ Open', kbF3, cmOpen,
          NewStatusKey('~F4~ New',  kbF4, cmNew,
          NewStatusKey('~Alt+F3~ Close', kbAltF3, cmClose,
          GetStatusKey(nil)))),
      NewStatusDef($F000, $FFFF,
          NewStatusKey('~F6~ Next', kbF6, cmOrderNext,
          NewStatusKey('~Shift+F6~ Pref', kbShiftF6, cmOrderPrev,
          GetStatusKey(nil))),nil))));
end;

Ah... you get the idea now. GetStatusKey serves as a "shorthand" for default status keys and the rest are defined differently. One quick thing before recombining: Since you use pretty lot of things, you need to modify your uses clause into:

uses App, Objects, Menus, Drivers, Views;

In case if you wonder, the string inside the tilde (~) are highlighted in the status line. That's it and enjoy your new status bar. Wait, wait, wait. Your status bar doesn't do anything. Don't worry, we'll make it alive in a few moments.

 

Menu Bar

As you may have already guessed: Menu bars are extended the same way as the status line's. Just inherit the virtual method and code some stuff there. The name is also analoguous: InitMenuBar. The definition is also similar: Using linked list. For nested and complex menu, the menu bar definition is dauntingly complex. So, it's better for us to create some functions to help breaking it down to several chunks.

The InitMenuBar will look something like this:

procedure TMyApp.InitMenuBar;
var R: TRect;
begin
  GetExtent(R);
  R.B.Y := R.A.Y + 1;
  MenuBar := new (PMenuBar, Init(R, NewMenu(
                 NewSubMenu('~F~ile', hcNoContext, NewMenu(GetFileMenuItems(nil)),
                 NewSubMenu('~E~dit', hcNoContext, NewMenu(GetEditMenuItems(nil)),
                 NewSubMenu('~O~rders', hcNoContext, NewMenu(GetOrdersMenuItems(nil)),
                 NewSubMenu('O~p~tions', hcNoContext, NewMenu(GetOptionsMenuItems(nil)),
                 NewSubMenu('~W~indow', hcNoContext, NewMenu(GetWindowMenuItems(nil)),
                 NewSubMenu('~H~elp', hcNoContext, NewMenu(GetHelpMenuItems(nil)), nil)))))))));
end;

Hmm... how similar. The first two statements are just like the ones in InitStatusLine. Get the extent, but now add the lower Y coordinate by one (since menu bars are streched one line down from the top). The submenus here are the ones displayed on the top. then the menu items of each of these are defined in the respective functions. About hc... constants as in hcNoContext, they define help context constants. The constant hcNoContext simply tell that this particular submenu has no help context. OK, let's look into its submenu items:

function GetFileMenuItems(Next: PMenuItem): PMenuItem;
begin
  GetFileMenuItems := NewItem('~N~ew',  'F2', kbF2, cmNew, hcNew,
                      NewItem('~O~pen', 'F3', kbF3, cmOpen, hcOpen,
                      NewLine(
                      NewItem('E~x~it', 'Alt+X', kbAltX, cmQuit, hcQuit, nil))));
end;

You can quickly notice that NewItem defines new menu item that consists of six elements:

You can observe too that NewLine provide menu separator line. The rest of the gangs are just way similar. I just leave them for you to experiment. :-) Oh, by the way, if you don't want to assign a shortcut key for a particular menu item, just use kbNoKey.

 

Bringing Menus and Status Lines to Life

Okay, so far that you created some "dummy" status bar and menu bars. Now, it's time to bring it to life. TV employs event-driven programming approach. So, instead of actively looping around to get the user input, you just inherit HandleEvent method from TApplication and just wait to be invoked whenever user feed some input (either by keyboard or mouse). This is nice because looping around wastes some computer time. (P.S.: Note that this is not entirely true since some of these are employing some looping technique underneath).

To simplify things, just consider event handling programming style is that to inherit a method that later be invoked (by TV, of course) whenever an event occurs. The event can be user inputs and so on. Let's look at the general skeleton on how inheriting HandleEvent:

procedure TMyApp.HandleEvent(var Event: TEvent);
begin
  inherited HandleEvent(Event);
  if Event.What = evCommand then
  begin
    case Event.Command of
       cmBlah: begin
                 :
                 ClearEvent(Event);
               end;
       cmFoo:  begin
                 :
                 ClearEvent(Event);
               end;
               :
               :  {etc}
               :
    end;
  end;
end;

Aha! That's easy. The first statement is mandatory. inherited HandleEvent(Event); simply denotes that we call the TApplication's HandleEvent to deal with default stuffs before continuing. Then the if part simply to detect whether the event is a command event (i.e. if menu is clicked or keyboard shortcut is pressed). Then, we do case..of to detect which command constant (that is previously defined in status line or menu bar) is actually invoked. After that, do the appropriate actions. The last statement for each command should be ClearEvent(Event); to mention that the event has been handled.

 

Enabling and Disabling Commands

You can enable and disable certain commands. Use DisableCommands to disable the commands and EnableCommands to enable them. For example:

DisableCommands([cmOrderWin, cmNew, cmOpen]);

EnableCommands is just similar. Just remember that the command constants of 0 to 255 can be disabled and the rest (256 to 65,536) can not. So, pay attention on defining those constants.

 

Closing

Ah! Refreshing memories from the remnant of my brain :-). It's been a long long time since I last programmed using TV. I hope that you get the idea on how TV simplifies a lot of stuff. Defining menu and status lines and bringing them to life is a snap here, right? Imagine if you have to define them manually.

There is one drawback: People can quickly recognize that all TV-based programs look way too similar. That's true. That's also the reason why I hate TV. But, who cares? As long as it can give me a quick solution on building apps, let it be :-).

Okay, see you in the next chapter.

 


Where to go

Chapter 8
News Page
Pascal Lesson 3 index
Contacting Me


Roby Joehanes © 2001