Организация меню в программах | |
Меню в текстовом режиме | Ошибки при организации меню |
Меню в графическом режиме | |
Пример организации меню из файлов |
Одним из, пожалуй, самых часто задаваемых на форумах вопросов, является вопрос о создании меню, ведь мало заставить программу
работать (и к тому же работать правильно) - надо еще внести в программу средства коммуникации с пользователем...
Здесь я опишу принцип создания простейшего меню в приложении. Но вначале - об основных ошибках, допускаемых начинающими программистами
при создании меню. Итак...
1. Ошибки при организации меню
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, т.е. аварийно... Да и стек не бесконечен - на каком то этапе он может оказаться переполненным, и тогда программа также завершится аварийно, но уже сама, без инструкции пользователя... А ведь приведенный фрагмент - не просто так выдуман мной... Он встречался, например, здесь:
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) Эта программа немного более сложна, чем приведенная выше...
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 (Форум "Интересные задачи для программистов")...
Пример организации меню из файлов
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-файлов с описанными в них меню) находится в архиве: