Организация меню в программах
   
Меню в текстовом режиме Ошибки при организации меню
Меню в графическом режиме  
   
Пример организации меню из файлов  

Одним из, пожалуй, самых часто задаваемых на форумах вопросов, является вопрос о создании меню, ведь мало заставить программу работать (и к тому же работать правильно) - надо еще внести в программу средства коммуникации с пользователем...

Здесь я опишу принцип создания простейшего меню в приложении. Но вначале - об основных ошибках, допускаемых начинающими программистами при создании меню. Итак...


1. Ошибки при организации меню

Первая, и самая основная ошибка при организации интерфейса программы - это полное перемешивание собственно вычислений и отрисовки элементов оформления. Такие программы обычно мало структурированны, в основном написаны методом "Copy/Paste", и очень большие по объему. Разобраться в алгоритме работы таких программ очень сложно, потому, что самого алгоритма как раз и не видно из-за многочисленных инструкции по установке цветов вывода информации и элементов оформления (TextColor/TextBackGround в текстовых режимах и SetColor/SetBkColor/SetFillStyle и им подобные - в графических)... Такого типа программы можно очень сильно уменьшить в размерах (если, конечно, все-таки удастся прорваться сквозь дебри кода и понять логику работы программы) правильным проектированием ее структуры... Как-то мне удалось программу размером в 45К ужать до 4К, но это - своеобразный рекорд, обычно размеры исходного текста уменьшаются в 2-3 раза...

Вторая ошибка заключается в том, что при работе с меню программист (в основном, начинающий) не подозревает, что он организовал бесконечную рекурсию... Например, вот такой фрагмент программы:
procedure proc_1;
begin
  { Действия по первому пункту меню }
  ...
  menu; { <--- !!! }
end;
procedure proc_2;
begin
  { Действия по второму пункту меню }
  ...
  menu; { <--- !!! }
end;

procedure menu;
begin
  ...
  case Choice of
    1: proc_1;
    2: proc_2;
  end;
  
end;
и даже то, что программа в таком виде не будет компилироваться, для того, чтобы компиляция все-таки произошла - надо добавить опережающее описание
procedure menu; forward;
не останавливает программиста... в результате получаем никогда не заканчивающуюся рекурсию, выйти из которой можно только выходом из программы через Exit/Halt, т.е. аварийно... Да и стек не бесконечен - на каком то этапе он может оказаться переполненным, и тогда программа также завершится аварийно, но уже сама, без инструкции пользователя... А ведь приведенный фрагмент - не просто так выдуман мной... Он встречался, например, здесь:
Форум "Все о Паскале" -> Странная ошибка, ну никак не одолеть...

Ну, а теперь, приступим все-таки к построению меню, лишенного этих недостатков... Программы подробно прокомментированы, так что разобраться, как именно они работают, я думаю, труда не составит...


2. Меню в текстовом режиме

За основу можно взять вот такой пример:
uses crt;
Type
   MenuType = (Vertical, Horizontal);

const
   width = 12; { В поле такой ширины будут выводиться пункты меню }
   nItems = 4; { Количество элементов меню                        }

   { Собственно, те надписи, которые будут появляться на экране   }
   optText1: array[0 .. pred(nItems)] of string = (
     'Option #1', 'Option #2', 'Option #3', 'Exit'
   );

   optNormal = LightGray; { Цвет выделенного элемента             }
   optSelected = Yellow;  { Цвет невыделенного элемента           }

var
   X, Y,
   selected,  { индекс элемента, который в настоящий момент являемся подсвеченным }
   row: integer;
   _style: menuType;  { Тип представления меню: вертикальное (Vertical)
                        или горизонтальное (Horizontal) }


{ Отрисовка всех элементов меню с выделением цветом одного из них - выбранного на данный момент }
procedure MakeMenu (optText: array of string; MaxItems: integer);
var
   i, _X: byte;
begin
     Y := row;
     _X := X;
     for i := 0 to MaxItems-1 do
     begin
          GoToXY (_X, Y);
          { Вот тут происходит выделение цветом "активного" элемента }
          if i = selected then
             TextColor (optSelected)
          else
             TextColor (optNormal);
          write (optText[ i ]);

          If _style = Horizontal Then
            inc (_X, width + 1)
          Else
            inc (Y, 2);
     end;
end;

{ Основная функция в нашем меню - позволяет перемещаться по пунктам и возвращает
  номер элемента, выбранного пользователем                                       }
function MenuOption (optText: array of string; MaxItems: integer): byte;
var
   ch: char;
begin
   selected := 0;

   If _style = Vertical Then Begin
     X := (80 - width) div 2;
     row := (25 - MaxItems) div 2;
   End
   Else Begin
     X := (80 - MaxItems * width) div 2;
     row := 2; { строчка, в которой будет находиться горизонтальное меню }
     GotoXY(1, row); ClrEol; { Очистка заданной строки для вывода горизонтального меню }
     End;

     repeat
           { Отрисовываем элементы меню }
           MakeMenu (optText, MaxItems);

           { И по нажатию клавиш увеличиваем/уменьшаем индекс текущего элемента }
           ch := readkey;
           if ch = #0 then
              ch := readkey;

           case ch of
           #80, #77: {Down/Right}
           begin
                inc (Selected);
                if Selected = MaxItems then
                   Selected := 0;
                MakeMenu (optText, MaxItems);
           end;

           #72, #75: {Up/Left}
           begin
                dec (Selected);
                if Selected < 0 then
                   Selected := MaxItems-1;
                MakeMenu (optText, MaxItems);
           end;
           end;
     until ch = #13; {Enter}

     {
       Если мы пришли сюда - значит, пользователь нажал Enter,
       и в переменной selected находится индекс выбранного им
       элемента меню
     }
     MenuOption := Selected + 1;

     {
       Восстанавливаем нормальный цвет вывода,
       и для вертикального меню очищаем экран
     }
     TextColor (optNormal);
     If _style = Vertical Then
        clrscr;
end;

{
  Процедуры, запускаемые при выборе пользователем определенных пунктов меню ...
  Собственно, эти процедуры и являются "рабочими лошадками", и именно в них нужно
  программировать те действия, которые требуются по алгоритму решения задачи.
}
procedure Proc_1;
begin
  ClrScr;
  WriteLn('#1 selected ...');
  ReadLn;
end;

procedure Proc_2;
begin
  ClrScr;
  WriteLn('#2 selected ...');
  ReadLn;
end;

procedure Proc_3;
begin
  ClrScr;
  WriteLn('Other selected ...');
  ReadLn;
end;

var
  Option: byte; { Эта переменная будет хранить номер пункта, выбранного пользователем }

begin
     { Проверяем с вертикальным меню (_style := Horizontal для горизонтального) }
     _style := Vertical;
     repeat

       clrscr;
       Option := MenuOption (optText1, nItems);

       case option of
         1: Proc_1;
         2: Proc_2;
         3: Proc_3;
       end;

     {
       Здесь я исходил из предположения, что завершающим пунктом меню всегда идет "Выход".
       Если это не так - надо просто подставить вместо nItems номер пункта для выхода из программы
     }
     until Option = nItems;

end.
(С) Совместная разработка Volvo877 и Romtek (forum.sources.ru)


3. Меню в графическом режиме

Эта программа немного более сложна, чем приведенная выше...

Отчасти это связано с тем, что реализация меню в графике - немного более сложная задача, чем то же самое действие в текстовом режиме. Но кроме этого, в программе используются продвинутые возможности Паскаля (в частности, процедурные типы, вложенные подпрограммы), которые немного увеличивают размер кода, но при этом делают его гораздо более структурированным, и упрощают его изменение в случае необходимости...

Само меню было выдрано из моей старой программы и немного доработано, поэтому его пункты именно такие...
Uses Graph, Crt;

{ Инициализация графики выделена в отдельную процедуру }
Procedure InitGraphix;
Var grDriver, grMode, ErrCode: Integer;
Begin
  grDriver := Detect;
  InitGraph(grDriver, grMode, '');
  ErrCode := GraphResult;
  If ErrCode <> grOk Then Begin
    WriteLn('Graphics error: ', GraphErrorMsg(ErrCode));
    WriteLn('Press "Enter" key to halt:');
    ReadLn; Halt(101)
  End;
  SetBkColor(Black);
  ClearDevice;
  
  { Устанавливаем маленький шрифт для удобочитаемости меню }
  SetTextStyle(smallfont, horizdir, 1);
End;

{
  Функциональный тип, который будет использован для изменения индекса 
  подсвеченного элемента (увеличения или уменьшения)
}
Type
  TFunc = Function(Var x: Byte): Byte;

{ Собственно функции, увеличивающие или уменьшающие параметр }
Function Incr(Var x: Byte): Byte; Far;
Begin
  Inc(x); Incr := x
End;
Function Decr(Var x: Byte): Byte; Far;
Begin
  Dec(x); Decr := x
End;

{
  Основная функция - 
  получает на входе массив строк (mainMenu), и их количество (Options),
  и возвращает индекс элемента, выбранного пользователем
}
Function menu(Const mainMenu: Array Of String;
              Const Options: Byte): Byte;
              
{
  Все константы внесены внутрь самой функции, что делает очень простым, например,
  вынесение этой функции в отдельный модуль
}

Const
  StartMenuX = 200;
  StartMenuY = 50;
  MenuWidthY = 18;

  { Клавиатурные коды для используемых в меню клавиш }
  kbEnter   = #13;
  kbEsc     = #27;
  kbHome    = #71;
  kbEnd     = #79;
  kbUp      = #72;
  kbDown    = #80;

  {
    Функция Index предназначена для очень быстрого перехода от работы с открытым
    массивом (open array) строк, к работе с определенным типом, описанным, например, так:
    Type StrAtrray = Array[1 .. 20] of String;
    
    В частности, Турбо Паскаль 6.0 открытые массивы не поддерживает, и чтобы не искать,
    где в программе происходит обращение к элементу массива, чтобы наладить работу программы
    при индексации с единицы, достаточно просто изменить функцию Index...
    
    Еще одна причина - массив с индексацией, начинающейся НЕ с 0, может быть описан во внешнем
    модуле, исходники которого недоступны, тогда опять же корретирование одной функции решает
    проблему
  }

  Function Index(X: Byte): Byte;
  Begin
    Index := X - 1;
  End;

  Function GetYPos(i: Byte): Word;
  Begin
    GetYPos := StartMenuY + i * MenuWidthY
  End;

  { Отрисовка элемента i подсвеченным (выбранным) }
  Procedure SetChoiseColor(i : Byte);
  Begin
    SetColor(White);
    OutTextXY(StartMenuX, GetYPos(i), mainMenu[Index(i)]);
  End;

  { Отрисовка элемента i как неподсвеченного }
  Procedure ResetChoiseColor(i : Byte);
  Begin
    SetColor(LightRed);
    OutTextXY(StartMenuX, GetYPos(i), mainMenu[Index(i)]);
  End;

  { Отрисовка всех элементов меню без подсветки }
  Procedure RefreshWindow;
  Var i : Byte;
  Begin
    For i := 1 To Options Do
      ResetChoiseColor(i)
  End;

Var
  { Здесь хранится индекс выбранного на данный момент элемента (начиная с 1-цы) }
  MenuResult : Byte;

  {
    В завсисмости от переданного параметра функция подсвечивает предыдущий/следующий
    элемент меню, и возвращает Истину... Зачем - см. ниже
  }
  Function GetUpdated(func: TFunc): Boolean;
  Begin
    ResetChoiseColor(MenuResult);
    SetChoiseColor(func(MenuResult));
    GetUpdated := True;
  End;

  { Перемещение подсветки вверх - если текущий элемент НЕ первый, то результатом будет True иначе False }
  Function MarkerUp : Boolean;
  Begin
    markerUp := False;
    If menuResult > 1
      Then markerUp := GetUpdated(Decr)
  End;
  { То же самое касается и перемещения подсветки вниз - если сейчас подсвечен НЕ последний, то результат - True }
  Function MarkerDown : Boolean;
  Begin
    markerDown := False;
    If menuResult < Options
      Then markerDown := GetUpdated(Incr)
  End;

{ Собственно, начинается функция Menu... Все, что было выше - дополнительные подпрограммы }

Var
  StopIt: Boolean;
Begin
  ClearDevice;
  SetTextStyle(SmallFont, HorizDir, 4);
  SetTextJustify(LeftText, CenterText);

  {
    Чертим рамочку в которой будет располагаться меню...
    
    Чтобы не усложнять программу еще больше, я не стал высчитывать координаты
    этой рамки программно, а просто жестко прописал из в коде
  }
  Rectangle(185, 45, 400, 400);

  RefreshWindow;
  MenuResult := 1;
  SetChoiseColor(1);
  StopIt := False;
  Repeat

    Case ReadKey Of
      #0 : Begin
             Case ReadKey Of
               kbUp   : If MarkerUp Then ;
               kbDown : If MarkerDown Then ;
               kbHome : While MarkerUp Do ;
               kbEnd  : While MarkerDown Do ;
             End;
           End;
      kbEnter :
        StopIt := True;
    End;

  Until StopIt;
  
  {
    Точно так же, как и в случае с текстовым меню - сюда добираемся только
    тогда, когда пользователь нажал Enter... Все, что остается - вернуть индекс
    выбранного элемента...
  }
  
  Menu := menuResult;
End;

var
  user_choice: byte;
const
  Options = 14;
  str_menu: Array[1 .. Options] Of String = (

                   '  EXAMPLE >>> SINUS   ',
                   '  EXAMPLE >>> TANGENS ',
                   '  COSINUS - COSINUS   ',
                   '  COSINUS - SINUS     ',
                   '  FIRST EXPONENTIAL   ',
                   '  SINUS - SINUS       ',
                   '  SINUS - COSINUS     ',
                   '  SECOND EXPONENTIAL  ',
                   '  ELLIPSOID           ',
                   '  BAD ELLIPSOID       ',
                   '  ELLIPT. PARABOLOID  ',
                   '  HYPER. PARABOLOID   ',
                   '  NO THIRTEEN !       ',
                   '  EXIT                '
                                                  );

{ Ну, и наконец - основная программа ... }
begin
  InitGraphix;
  
  { Повторять ... }
  Repeat

    { Получаем индекс выбранного пользователем элемента меню }
    user_choice := menu(str_menu, Options);

    (*
    {
      Здесь обрабатываем - совершенно аналогично работе с текстовым меню,
      выбором соответствующей процедуры в зависимости от user_choice, я этого
      здесь не делаю - только отрисовываю меню ...
    }
    
    case user_choice of
      { ... }
    end;
    *)

  { ... пока пользователь не выберет "Выход"}
  Until user_choice = Options;
  CloseGraph;
end.
(С) volvo877 (Форум "Интересные задачи для программистов")...




Пример организации меню из файлов

Задача:
Недавно на одном из форумов встретился вот такой вопрос:

"Здравствуйте.
У меня такой вопрос. Есть меню из 3-х пунктов. Если нажимается первый пункт, то должно появиться следующее меню, где также можно выбирать пункты второго меню. Содержимое второго меню хранится в файле. То есть при нажатии на первый пункт главного меню я вывожу пункты второго меню из файла. Как сделать, чтобы из этих (выведенных из файла) пунктов также можно было выбирать... или это невозможно?
Спасибо заранее за помощь."


Эта задача показалась мне достаточно интересной, чтобы привести ее решение. Итак...

Начнем - с того, что в данном случае работать надо будет через процедурные типы. Причем, для того чтобы сделать меню максимально гибким, надо не просто передавать в функцию MenuOption массив строк, это меню образующих, а передавать вместе с этим еще и процедуру, которая должна выполняться при выборе данного элемента меню; для того, чтобы сделать программу максимально гибкой, лучше добавить еще одну процедуру - ShowMenu - в которой и будет выполняться ассоциированная с пунктом меню процедура.

Сначала - о том, как, собственно, будут храниться теперь пункты меню (да еще вместе с процедурами обработки):
type
  { описываем тип - процедуру, которая принимает единственный параметр - строку... }
  TProcedure = procedure(const args: string);
  { ... и тип - строку (для экономии памяти ограничим ее 50 символами) }
  TMyString = string[50];

  { А теперь объединим эти данные "под одной крышей" }
  TMenuAction = record
    s: TMyString;         { <--- Пункт меню                                                              }
    procArgs: TMyString;  { <--- Параметр, который передастся в процедуру                                }
    Proc: TProcedure;     { <--- Собственно процедура, вызываемая при выборе вышеозначенного пункта меню }
  end;
Теперь - немного о том, какой формат должны иметь файлы, содержащие меню ( вы еще не забыли, что основная задача - в том, чтобы читать меню из файла и отображать его :) ). Будем считать - для простоты - что все, что можно будет сделать в меню - это либо запустить другое меню (из другого файла), либо распечатать какой-либо файл, либо ничего не делать (пустая процедура)...

В таком случае нам достаточно хранить в файлах-"описателях меню" данные в таком вот формате:
Пункт меню 1|Mmenu01.txt  { <--- M - чтобы вызвать меню из другого файла (здесь menu01.txt)        }
Пункт меню 2|Pfile01.txt  { <--- P - чтобы распечатать содержимое другого файла (здесь file01.txt) }
exit|F                    { <--- F - ничего не делать, заглушка                                    }
Для того, чтобы реализовать чтение меню из файла и его запуск, понадобится следующая функция:
const
  max_lines_in_menu = 15;
type
  PStrArr = ^strArr;
  strArr = array[1 .. max_lines_in_menu] of TMenuAction;

{
  f_name - имя файла, который нужно обработать;
  size   - (возвращается) количество строк в меню
  
  Результат работы функции - указатель на массив из size элементов типа TMenuAction
}
function Prosmotr(f_name: string; var size: integer): PStrArr;
var
  f: text;
  s: string;
  i: integer;
  p: PStrArr;
begin
  assign(f, f_name); reset(f);    { <--- Открываем "рабочий" файл    }

  size := 0;                      { Для подсчета числа строк в файле }
  while not seekeof(f) do begin   { Пока есть строки ... }
    inc(size); readln(f);         { ... подсчитываем их. }
  end;
  reset(f);                       { После окончания подсчета - переоткроем файл }
  

  { Выделяем память под size элементов }
  getmem(p, size * sizeof(TMenuAction));
  i := 1;
  while not eof(f) do begin
    readln(f, s);                            { Читаем строки из файла                                      }
    p^[i].s := copy(s, 1, pos('|', s) - 1);  { Пункт меню - это содержимое строки до символа '|'           }
    delete(s, 1, pos('|', s));               { Удаляем из прочитанной строки начало, включая и разделитель }
    case UpCase(s[1]) of                     { А теперь - смотрим, какую операцию надо будет делать        }
      'M':                                   { M - создать новое меню из файла }
      begin
        p^[i].Proc := makeMenuProc;          { вызываться будет процедура MakeMenuProc                     }
        p^[i].ProcArgs := copy(s, 2, 255);   { в качестве параметра ей будет передано продолжение строки   }
      end;
      'P':                                   { P - распечатать какой-либо файл }
      begin
        p^[i].Proc := printFileProc;         { вызываться будет PrintFileProc                              }
        p^[i].ProcArgs := copy(s, 2, 255);   { в качестве параметра ей будет передано продолжение строки   }
      end;
      'F':                                   { F - Foo action - ничего не делать       }
      begin
        p^[i].Proc := doNothingProc;         { вызываться будет "ничего_не_делающая" doNothingProc         }
        p[i].ProcArgs := '';                 { параметры ей не нужны, так что передадим пустую строку      }
      end;
    end;

    inc(i);
  end;
  close(f);
  Prosmotr := p;                             { Не забываем вернуть указатель !!! }
end;
Ну, и для полноты картины не хватает только реализации ShowMenu, в которой и будет происходить запуск процедуры для каждого пункта меню. Я бы реализовал ее так:
procedure ShowMenu(const ar: array of TMenuAction;
                   const nItems: integer);
var Option: integer;
begin
  _style := Vertical;
  repeat

    clrscr;
    Option := MenuOption(ar, nItems);
    ar[Pred(Option)].Proc(ar[Pred(Option)].ProcArgs); { <--- Вот, собственно, основная идея }

  until Option = nItems; { Последним пунктом меню ВСЕГДА должен быть возврат }
end;
Полностью исходник (вместе с примером TXT-файлов с описанными в них меню) находится в архиве:
f_menu.rar




Все вопросы (как по приведенным выше программам, так и любые вопросы по программированию вообще) - задавайте на форум...



Free Web Hosting