Achieving PC Display Independence
----------------------------------------------------------
Marc Lalibert 
Sherbrooke, Qubec, Canada



Last summer, I was writing a program to integrate a system of differential
equations on a PC. I was trying to do something that would look professional.
This meant I needed three things: (1) an on-line help system; (2) a clean menu
( which would eventually yield BIGMENU, a program available on a TUG disk );
and (3) the ability to run with any kind of PC monitor. This capability is the
subject of this story.

The problem is a familiar one: to build a program that will run well on most,
if not all, of the PC's monitors. At the time I was using an EGA monitor, a
CGA with a color monitor, another CGA with a composite amber monitor, a
monochrome monitor and another monochrome with a Hercules card. This is a
programmer's nightmare! After trying for some time to find a combination of
color and background that would look good on all these screens, I found that
there is NO combination (other than white on black and black on white ) that
looks good on all of them. Back to square one!

Since it was impossible to have an all-purpose choice of color, I decided to
use variable colors instead of constant ones. The value of the variables would
be determined at run-time, depending on the kind of hardware installed.

This called for a procedure to check what kind of hardware was actually used.
I then gathered some information on how to do that, mainly in old issues of
TUG Lines, routines from the Turbo Graphix Toolbox and "The Peter Norton
Programmer's Guide to the IBM PC".

To keep track of the kind of screen used, I decided to use a scalar type :

  TypeOfScreen = ( HGC,IBMCGA,IBMMono,Unknown );

where HGC stands for Hercules Graphic Card, IBMCGA is the Color Graphic
Adapter, IBMMono is for the monochrome adapter, and Unknown is for everything
else. I next defined a GLOBAL variable Screen 

  Screen : TypeOfScreen;

(The procedure SelectTheScreen follows in Listing 1.)

The memory at hex location 0000:0449 specifies the current video mode.
Interrupt $11 will access the equipment list data. In the procedure we check
to see if the initial video mode is 80 column monochrome. (For more
information on this topic refer to a book like "The Peter Norton Programmer's
Guide to the IBM PC").

Note that you must take care to check for the HGC before checking for the
monochrome adapter because the value of the memory at hex 0000:0449 is the
same for both of them. Since those two cards require a different combination
of colors and backgrounds for reverse video, you must be able to differentiate
between them.

The next step was to select a choice of colors and backgrounds for each type
of screen. I used six GLOBAL variables:

  NonSelectedColor, NonSelectedBG, SelectedColor, SelectedBG,
  TheTextColor, TextBG : BYTE;

Three variables have the suffix 'BG': this stands for BackGround. Note that I
use 'TheTextColor' for the name of the text color, and not TEXTCOLOR, which is
a standard procedure in Turbo Pascal. The procedure SelectTheColors follows in
Listing 2.

This specific choice of colors has been found to be acceptable for most kinds
of screens. The procedure SelectTheScreen is, however, unable to differentiate
between a true CGA, a CGA on a composite video monitor and an EGA. It would be
possible to add to SelectTheScreen a few more tests to determine a few more
kinds of screens, but I don't think it would ever be perfect. 

It is also obvious that my particular choice of colors will not please
everybody. For this reason I added a procedure called EnterANewValue, found in
Listing 3, which allows the user of the program to make his or her own
choices. I believe that all programs should have this feature, because it
allows the user to have total control of what the screen will look like.

Here is how you use these procedures: first you call SelectTheScreen, which
will give you a good guess at the kind of screen the program is running on.
Then you call SelectTheColors and you now have a good idea of the colors you
should use.

Each time you have to write something on the screen you call the two
procedures TEXTCOLOR and TEXTBACKGROUND with the pertinent parameters (for
text, use TheTextColor and TextBG; for something less important, use
NonSelectedColor and NonSelectedBG, etc.). 

I strongly recommend that after writing anything on the screen in a color
other than the text color that you reinitialize to the text color and
background --  especially if the background has changed. If you don't do this,
the next CLRSCR will change the background of the active window to the current
background. A typical use might look like Listing 4.

And somewhere in your program you should let the user make his or her own
choice of colors by using EnterANewValue!


Listing 1 :

PROCEDURE SelectTheScreen;

{ This procedure will try to determine what kind of screen is been used. It
  has been inspired by many sources, including TUG and Turbo Graphix Toolbox }

TYPE
  TypeRegs = RECORD CASE INTEGER OF
    1 : ( AX,BX,CX,DX,BP,SI,DI,DS,ES,FLAGS : INTEGER );
    2 : ( AL,AH,BL,BH,CL,CH,DL,DH : BYTE );
  END;

VAR
  EGASwitch,Info : BYTE;
  EquipFlag,I : INTEGER;
  Regs : TypeRegs;

  FUNCTION HercPresent : BOOLEAN;

  { This function have been borrowed from the Turbo Graphix Toolbox }

  BEGIN
    Inline($BB/$00/$01/$BA/$BA/$03/$EC/$88/$C4/$80/$E4/$80/$B9/$40/$00/$EC/
           $24/$80/$38/$E0/$E1/$F9/$75/$05/$4B/$75/$F1/$EB/$33/$B8/$00/$B0/
           $8E/$C0/$E8/$11/$00/$75/$0B/$B0/$01/$BA/$BF/$03/$EE/$E8/$06/$00/
           $74/$1E/$B0/$01/$EB/$1C/$26/$8A/$1E/$FF/$7F/$26/$8A/$0E/$FF/$3F/
           $26/$FE/$06/$FF/$3F/$26/$3A/$1E/$FF/$3F/$26/$88/$0E/$FF/$3F/$C3/
           $30/$C0/$88/$46/$04/$08/$C0);
  END;

BEGIN
  WITH Regs DO
  BEGIN
    INTR( $11,Regs );
    EquipFlag := AX;
  END;
  IF HercPresent AND ( ( EquipFlag AND $30 ) = $30 ) THEN Screen := HGC
  ELSE Screen := Unknown;
  IF Screen = Unknown THEN
  BEGIN
    IF MEM[ $0000:$0449 ] = $07 THEN Screen := IBMMono;
  END;
  IF Screen = Unknown THEN
  BEGIN
    IF ( ( EquipFlag AND $30 ) IN [ $10,$20 ] ) THEN Screen := IBMCGA;
  END;
END; { PROCEDURE SelectTheScreen }


Listing 2 :

PROCEDURE SelectTheColors;
{ This will select the value of NonSelectedColor, SelectedColor and 
  TheTextColor and their backgrounds according to the value of Screen. }

BEGIN
  CASE Screen OF
    HGC : BEGIN
      NonSelectedColor := BLUE;
      NonSelectedBG := BLUE;
      SelectedColor := BLACK;
      SelectedBG := LIGHTGRAY;
      TheTextColor := BLUE;
      TextBG := BLUE;
    END;
    IBMCGA : BEGIN
      NonSelectedColor := LIGHTGREEN;
      NonSelectedBG := BLACK;
      SelectedColor := YELLOW;
      SelectedBG := BLACK;
      TheTextColor := LIGHTRED;
      TextBG := BLACK;
    END;
    IBMMono : BEGIN
      NonSelectedColor := LIGHTGRAY;
      NonSelectedBG := BLACK;
      SelectedColor := BLACK;
      SelectedBG := LIGHTGRAY;
      TheTextColor := WHITE;
      TextBG := BLACK;
    END;
    Unknown : BEGIN
      NonSelectedColor := WHITE;
      NonSelectedBG := BLACK;
      SelectedColor := BLACK;
      SelectedBG := WHITE;
      TheTextColor := WHITE;
      TextBG := BLACK;
    END;
  END; { CASE Screen }
END; { PROCEDURE SelectTheColors }

Listing 3 :

PROCEDURE EnterANewValue( BG : BOOLEAN; VAR Color : BYTE );
{ This procedure will ask the user to enter a new value for the selected
  color.

If BG is TRUE we wish to read a background value ( between 0 and 7 ), else
we wish to read a color value ( between 0 and 15 ). 

You call EnterANewValue like this :

  EnterANewValue( TRUE,TextBG ) or
  EnterANewValue( FALSE,SelectedColor ) }

VAR
  ColorDummy : INTEGER;

BEGIN
  CLRSCR;
  WRITELN( 'The actual value is : ',Color:2 );
  WRITELN;
  WRITE( 'Enter a new value, between 0 and ' );
  IF BG THEN WRITE( '7 : ' ) ELSE WRITE( '15 : ' );
  READLN( ColorDummy );
  WRITELN;
  WHILE ( BG AND ( ColorDummy > 7 ) ) OR ( NOT BG AND ( ColorDummy > 15 ) )
  OR ( ColorDummy < 0 ) DO
  BEGIN
    IF ColorDummy < 0 THEN
    BEGIN
      WRITELN( 'The value must be higher or equal to 0. Try again' );
    END
    ELSE BEGIN
      IF BG THEN
      BEGIN
        WRITELN( 'The value must be lower or equal to 7. Try again' )
      END
      ELSE BEGIN
        WRITELN( 'The value must be lower or equal to 15. Try again' )
      END;
    END;
    READLN( ColorDummy );
  END; { WHILE }
  Color := LO( ColorDummy );
END; { PROCEDURE EnterANewValue }


Listing 4 :

  CLRSCR;
  TEXTCOLOR( NonSelectedColor );
  TEXTBACKGROUND( NonSelectedBG );
  DrawASquare( 1,1,80,7 );
  DrawALine( 1,80,4 );
  TEXTCOLOR( TheTextColor );
  TEXTBACKGROUND( TextBG );
