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