Delphi и управление ресурсами

         

Требования к программе


В результате анализа различных тестов были сформулированы следующие требования к программе:

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

в текстовом файле;
программа должна быть инвариантна к различным тестам, т. е. изменения в тесте не должны вызывать требование изменения программы;
в программе должна быть заблокирована возможность возврата к предыдущему вопросу. Если вопрос предложен, то на него должен быть дан ответ.
На рис. 15.1 приведен пример диалогового окна программы тестирования во время ее работы.

Рис. 15.1. Диалоговое окно программы тестирования

Файл теста


Тест представляет собой последовательность вопросов, на которые испытуемый должен ответить путем выбора правильного ответа из нескольких предложенных вариантов.

Файл теста состоит из трех разделов:

раздел заголовка; раздел оценок; раздел вопросов.
Заголовок содержит общую информацию о тесте, например, о его назначении. Заголовок может состоять из нескольких строк. Признаком конца заголовка является точка, стоящая в начале строки.

Вот пример заголовка файла теста:

Сейчас Вам будут предложены вопросы о знаменитых памятниках и архитектурных сооружениях Санкт-Петербурга.

Вы должны из предложенных нескольких вариантов ответа выбрать правильный.

За заголовком следует раздел оценок, в котором приводятся названия оценочных уровней и количество баллов, необходимое для достижения этих уровней. Название уровня должно располагаться в одной строке. Вот пример раздела оценок:

Отлично

100

Хорошо

85

Удовлетворительно

60

Плохо

50

За разделом оценок следует раздел вопросов теста.

Каждый вопрос начинается текстом вопроса, за которым может следовать имя файла иллюстрации, размещаемое на отдельной строке и начинающееся символом \. Имя файла иллюстрации является признаком конца текста вопроса. Если к вопросу нет иллюстрации, то вместо имени файла ставится точка.

После вопроса следуют альтернативные ответы. Текст альтернативного ответа может занимать несколько строк. В строке, следующей за текстом ответа, располагается оценка (количество баллов) за выбор этого ответа. Если альтернативный ответ не является последним для текущего ответа, то перед оценкой ставится запятая, если последний — то точка.

Вот пример вопроса:

Какую формулу следует записать в ячейку В5, чтобы вычислить сумму выплаты?

\tab1.bmp

=сумма(В2-В4)

,0

=сумма(В2:В4)

,2

=В2+ВЗ+В4

.1

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

Ниже, в качестве примера, приведен текст файла вопросов для контроля знания истории памятников и архитектурных сооружений Санкт-Петербурга.

Сейчас Вам будут предложены вопросы о знаменитых памятниках и архитектурных сооружениях Санкт-Петербурга. Вы должны из предложенных нескольких вариантов ответа выбрать правильный.

Вы прекрасно знаете историю Санкт-Петербурга! 8

Вы много знаете о Санкт-Петербурге, но на некоторые вопросы ответили неверно . 7

Вы недостаточно хорошо знаете историю Санкт-Петербурга. 6

Вы, наверное, только начали знакомиться с историей Санкт-Петербурга? 5

Архитектор Исаакиевского собора:

\isaak.bmp

Доменико Трезини

,0

Огюст Монферран

,1

Карл Росси

.0

Александровская колонна воздвигнута как памятник, посвященный:

\aleks.bmp

деяниям императора Александра 1

,0

подвигу народа в Отечественной войне 1812 года

.1

Архитектор Зимнего дворца:

\herm.bmp

Бартоломео Растрелли

,1

Огюст Монферран

,0

Карл Росси

.0

Памятник русской военной славы собор Божией Матери Казанской (Казанский собор) построен по проекту русского зодчего:

A. Н. Воронихина

,1

И. Е. Старова

,0

B. И. Баженова .0

Остров, на котором находится Ботанический сад, основанный императором

Петром I, называется:

\bot.bmp

Заячий

,0

Медицинский

,0

Аптекарский

.1

Невский проспект получил свое название:

по имени реки, на берегах которой расположен Санкт-Петербург

, 0

по имени близко расположенной Александро-Невской лавры

,1

в память о знаменитом полководце Александре Невском

.0

Создатель скульптурных групп Аничкова моста. "Укрощение коня человеком":

\klodt.bmp

П. Клодт

,1

Э. Фальконе

.0

Скульптор знаменитого монумента "Медный всадник":

Э. Фальконе

,1

П. Клодт

.0

Файл теста может быть подготовлен в текстовом редакторе Notepad или Microsoft Word. В случае использования Microsoft Word при сохранении текста следует указать, что надо сохранить только текст. Для этого в диалоговом окне Сохранить в списке Тип файла следует выбрать вариант Только текст (*.txt).

Форма приложения


На рис. 15.2 приведен вид стартовой формы Forml во время разработки программы. Эта форма будет использоваться как для вывода вопросов теста и ввода ответов пользователя, так и для вывода начальной информации о тесте и результатов тестирования.

Поле метки Label3 предназначено для вывода текста вопроса, начальной информации о тесте и результатов тестирования.

Поля Label 1, Label2, Label3 и Label 4 предназначены для вывода текста альтернативных ответов, а переключатели RadioButtoni, RadioButton2, RadioButton3 и RadioButton4 — для выбора ответа.

Командная кнопка Buttonl предназначена для подтверждения выбора альтернативного ответа и перехода к следующему вопросу теста.

Следует обратить внимание на недоступный (невидимый) во время работы переключатель RadioButton5. Перед выводом очередного вопроса он программно устанавливается в выбранное положение, что обеспечивает сброс (установку в невыбранное состояние) переключателей выбора ответа (RadioButton1i, RadioButton2, RadioButton3 И RadioButton4).

Рис. 15.2. Форма приложения Test Значения свойств стартовой формы приведены в табл. 15.1.

Таблица 15.1. Значения свойств стартовой формы

Свойство

Значение Пояснение

Caption


Height

362

Width

562

Color

clWhite

Font . Name

Arial Cyr

Borderlcons . biSystemMenu

True Есть кнопка системного меню

Borderlcons . biMinimize"

False Нет кнопки Свернуть окно

Borderlcons . biMaximize

False Нет кнопки Развернуть окно

BorderStyle

bsSingle Тонкая граница окна, нельзя изменить размер окна

Следует обратить внимание, что несмотря на то, что свойства Border-icons. biMinimize И Borderlcons.biMaximize имеют значение False, кнопки свернуть окно и Развернуть окно отображены в форме. Реальное воздей-

ствие значения этих свойств на вид окна проявляется только во время работы программы. Значение свойства Borderstyle также проявляет себя только во время работы программы.

В табл. 15.2—15.5 приведены значения свойств компонентов формы.

Таблица 15.2. Значения свойств компонентов Label1 -Label5

Свойство

Компонент


Label1

Label2

Label3

Label 4

Label5

Left

32

32

32

32

32

Тор

64

96

128

160

8

AutoSize

True

True

True

True

True

Wordwrap

True

True

True

True

True

 

Таблица 15.3. Значения свойств компонентов RadioButton1 —RadioButton5

Свойство

Компонент



Radio-

Button1

Radio-

Button2

Radio-

Button3

Radio-

Button4

Radio-

Button5

Caption


-




Left

8

8

8

8

8

Top

64

96

128

160

174

Visible

True

True

True

True

False

Таблица 15.4. Значения свойств кнопки Button1

Свойство

Значение

Name

Buttonl

Caption

Ok

Left

13

Top

273

Height

28

Width

82

Таблица 15.5. Значения свойств панели Panel1

Свойство

Значение

Name

Panell

Caption


Height

46

Align

alBottom


Вывод иллюстрации


Для вывода иллюстрации в форму добавлен компонент image, значок которого (рис. 15.3) находится на вкладке Additional палитры компонентов. В табл. 15.7 приведены свойства компонента image.

Рис.15.3. Значок компонента Image

Таблица 15.6. Свойства компонента image

Свойство

Определяет

Name

Имя компонента

Picture

Свойство, являющееся объектом типа Tbitmap. Определяет выводимую картинку

Left

Расстояние от левого края формы до левой границы области картинки

Top

Расстояние от верхней границы формы до верхней границы области картинки

Height

Высоту картинки

Width

Ширину картинки

Stretch Признак автоматического сжатия или растяжения картинки таким образом, чтобы она была видна полностью в области, размер которой задан свойствами width и Height

AutoSize

Признак автоматического изменения размера компонента в соответствии с реальным размером картинки.

Если значение свойства AutoSize равно True, то при изменении значения свойства picture автоматически меняется размер области вывода иллюстрации так, чтобы была видна вся картинка.

Если значение свойства AutoSize равно False, а размер картинки превышает размер области, то отображается только часть картинки

Картинку, отображаемую в области image, можно задать во время создания формы или во время работы программы. Во время создания формы картинка задается установкой значения свойства Picture. Во время работы программы — Применением Метода LoadFromFile.

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

Image1.Picture.LoadFromFile('isaak.bmp');

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

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

Рис. 15.4. Вычисление размера области вывода иллюстрации

Если реальный размер иллюстрации превышает размер области, выделенной для ее вывода, то необходимо вычислить коэффициент масштабирования и установить максимально возможные, пропорциональные ширине и высоте иллюстрации, значения свойств width и Height области вывода иллюстрации.

Реальные размеры иллюстрации, загруженной в область image 1, можно получить из свойств Image1.Picture.Bitmap.Width И Image1.Picture.Bitmap.Height.


Загрузка файла теста


Передать имя файла теста программе тестирования можно через параметр командной строки путем настройки свойств значка, изображающего программу тестирования на рабочем столе или в папке.

Например, для настройки программы тестирования, значок запуска которой находится на рабочем столе, на работу с файлом теста Peterb.txt необходимо щелкнуть правой кнопкой мыши на значке программы, из появившегося контекстного меню выбрать команду Свойства и в поле Объект, после имени файла программы (Testl.exe), ввести имя файла теста (Peterb.txt), заключив его в двойные кавычки (рис. 15.5).

Рис. 15.5. Настройка программы тестирования

Примечание

Текст, находящийся в поле Объект вкладки Ярлык диалогового окна Свойства, называется командной строкой.

Программа может получить параметр, указанный в командной строке запуска программы, как значение функции ParamStr^), где л — номер параметра. Количество параметров командной строки находится в глобальной переменной ParamCount. Для приведенного выше примера командной строки запуска программы тестирования значение переменной ParamCount равно 1, а функции ParamStr (1) — peterb.txt.

Ниже приведен фрагмент программы, обеспечивающий прием параметра из командной строки:

if ParamCount = 0 then begin

ShowMessage('Ошибка! Не задан файл вопросов теста.');

goto bye; // аварийное завершение программы

end;

FileName := ParamStr(1); // имя файла — параметр командной строки

При запуске программы, использующей параметры командной строки, из среды разработки параметры нужно ввести в поле Parameters диалогового окна Run Parameters (рис. 15.6), которое открывается в результате выбора из меню Run команды Parameters.

Рис.15.6. Диалоговое окно Run Parameters


Текст программы


После создания формы в окно редактора кода, в секцию implementation следует поместить описание глобальных констант (раздел const) и переменных (раздел var). Затем можно приступить к созданию процедур обработки событий.

Их в программе три: обработка события onActivate для стартовой формы, обработка события Onclick для командной кнопки Buttoni и процедура обработки события onclick — одна, общая для переключателей выбора ответа.

В листинге 15.1 приведен полный текст программы.

Листинг 15.1. Программа тестирования

unit test1_;

interface

uses

SysUtils, WinTypes, WinProcs, Messages,

Classes, Graphics, Controls, Forms,

Dialogs, StdCtrls, ExtCtrls;

type

TForm1 = class(TForm) // вопрос

Label3: TLabel; // альтернативные ответы

Label1: TLabel; Label2: TLabel;

Label3: TLabel; Label4: TLabel;

// переключатели выбора ответа

RadioButton1: TRadioButton;

RadioButton2: TRadioButton;

RadioButton3: TRadioButton;

RadioButton4: TRadioButton;

Image1: TImage; // область вывода иллюстрации

Button1: TButton; // кнопка Ok, Дальше

RadioButtonS: TRadioButton; // "служебная" кнопка

Panel1: ТPanel;

procedure FormActivate(Sender: TObject);

procedure ButtonlClick(Sender: TObject};

procedure RadioButtonClick(Sender: TObject);

private

{ Private declarations }

public

{ Public declarations }

end;

var

Form1: TForm1; // форма

implementation

const

N_LEV=4; // четыре уровня оценки

N_ANS=4; // четыре варианта ответов

var

f:TextFile;

fn:string; // имя файла вопросов

1evel:array[1..N_LEV] of integer;

// сумма, соответствующая уровню

mes:array[1.,N_LEV] of string;

// сообщение, соответствующее уровню

score:array[1..N_ANS] of integer;

// балл за выбор ответа

summa:integer; // набрано очков

vopros:integer; // номер текущего вопроса

n_otv:integer; // число вариантов ответа

otv:integer; // номер выбранного ответа

// вывод начальной информации о тесте

procedure info(var f:TextFile;l:TLabel);

var

s,buf:string; begin

buf:='';

repeat

readln(f,s); if s[l] <> '.'

then buf := buf + s+ ' ';

until s[l] ='.'; l.caption:=buf;

end;

// прочитать информацию об оценках за тест

Procedure GetLevel(var f:TextFile);

var

i:integer; buf:string;

begin // заполняем значения глобальных массивов

i:=l;

repeat

readln(f,buf); if buf[1] <> '.' then begin

mes[i]:=buf; readln(f,level[i]);

i:=i+1;

end;

until buf[1]='.';

end;

// масштабирование иллюстрации

Procedure ScaleImage(Imagel:TImage);

var

w,h:integer; // максимально допустимые размеры картинки

scaleX:real; // коэф. масштабирования по X

scaleY:real; // коэф. масштабирования по Y

scale:real; // общий коэф. масштабирования

begin

// вычислить максимально допустимые размеры картинки

w:=Form1.ClientWidth-10;

h:=Form1.ClientHeight

- Form1.Panel1.Height -5

- Form1.Label5.Top

- Form1.Label5.Height - 5;

if Form1.Label1.Caption <> ''

then h:=h-Form1.Label1.Height-5;

if Form1.Label2.Caption <> ''

then h:=h-Form1.Label2.Height-5;

if Forml.Label3.Caption <> ''

then h:=h-Form1.Label3.Height-5;

if Forml.Label4.Caption <> "

then h:=h-Form1.Label4.Height-5; // определить масштаб

if w>Imagel.Picture.Bitmap.Width

then scaleX:=l

else scaleX:=w/Imagel.Picture.Bitmap.Width;

if h>Imagel.Picture.Bitmap.Height

then scaleY:=l

else scaleY:=h/Image1.Picture.Bitmap.Height;

if ScaleY<ScaleX

then scale:=scaleY

else scale:=scaleX; // здесь масштаб определен

Image1.Top:=Form1.Label5.Top+Form1.Labels.Height+5;

Image1.Width:=Round(Image1.Picture.Bitmap.Width* scale);

Image1.Height:=Round(Image1.Picture.Bitmap.Height*scale);

end;

// вывод вопроса на экран

Procedure VoprosToScr(var f:TextFile;frm:TForm1;var vopros:integer)

var

i:integer;

code:integer;

s,buf:string;

ifn:string; // файл иллюстрации

begin

vopros:=vopros+l ;

str(vopros:3,s) ;

frm. caption: = 'Вопрос' + s;

//выведем текст вопроса

buf:='';

repeat

readln(f, s) ;

if (s[l] <> '.') and (s[l] <>'\')

then buf:=buf+s+' ';

until (s[l] ='.') or (s[l] = '\');

frm. labels.caption:=buf;

if s[1] <> '\'

then Form1.Image1.Tag:=0 else

// к вопросу есть иллюстрация

begin

Form1.Image1.Tag:=1;

if n:=copy(s,2,length(s));

try

Form1.Image1.Picture.LoadFromFile(ifn)

except

on E:EFOpenError do

frm.tag:=0; end; // try end;

// читаем варианты ответов

i:=l;

repeat

buf: = ";

repeat // читаем текст варианта ответа

readln(f,s);

if (s[1]<>'. ') and (s[l] о ', ')

then buf:=buf+s+' ';

until (s[l]=',')or(s[1]='.'); // прочитан альтернативный ответ

val(s[2],score[i],code);

case i of

1: frm.Label1.caption:=buf;

2: frm.Label2.caption:=buf;

3: frm.Label3.caption:=buf;

4: frm.Label4.caption:=buf;

end;

until s[l]='.';

// здесь прочитана иллюстрация и альтернативные ответы

// текст вопроса уже выведен

if Forml.Image1.Tag =1 // есть иллюстрация к вопросу then

begin

Scalelmage(Form1.Image1); Form1.Image1.Visible:=TRUE;

end;

// вывод альтернативных ответов

if Form1.Label1.Caption <> " then begin

if Forml.Image1.Tag =1

then

frm.Label1.top:=frm.Imagel.Top+frm.Image1.Height+5

else

frm.Label1.top:=frm.Label5.Top+frm.Labels.Height+5;

frm.RadioButton1.top:=frm.Label1.top;

frm.Labell.visible:=TRUE;

frm.RadioButton1.visible:=TRUE;

end;

if Forml.Label2.Caption <> " then begin

frm.Label2.top:=frm.Label1.top+ frm.Label1.height+5;

frm.RadioButton2.top:=frm.Label2.top;

frm.Label2.visible:=TRUE;

frm.RadioButton2.visible:=TRUE;

end;

if Forml.Label3.Caption <> '' then begin

frm.Label3.top:=frm.Label2.top+ frm.Label2.height+5;

frm.RadioButtonS.top:=frm.Label3.top;

frm.Label3.visible:=TRUE;

frm.RadioButtonS.visible:=TRUE;

end;

if Forml.Label4.Caption <> '' then begin

frm.Label4.top:=frm.Label3.top+ frm.Label3.height+5;

frm.RadioButton4. top:=frm.Label4.top;

frm.Label4.visible:=TRUE;

fm.Rad±o8utton4.vis:tble:=TRUE]

end;

end;

Procedure ResetForm(frm:TForml);

begin

// сделать невидимыми все метки и переключатели

frm.Label1.Visible:=FALSE;

f rm.Label1.caption: ='';

frm.Label1.width:=frm.ClientWidth-frm.Label1.left-5;

frm.RadioButtonl.Visible:=FALSE;

frm.Label2.Visible:=FALSE;

frm.Label2.caption:='';

frm.Label2.width:=frm.ClientWidth-frm.Label2.left-5;

frm.RadioButton2.Visible:=FALSE;

frm.Label3.Visible:=FALSE;

frm.Label3.caption:='';

frm.Label3.width:=frm.ClientWidth-frm.Label3.left-5;

frm.RadioButton3.Visible:=FALSE;

frm.Label4.Visible:=FALSE;

frm.Label4.caption:='';

frm.Label4.width:=frm.ClientWidth-frm.Label4.left-5;

f rm.RadioButton4.Visible:=FALSE;

frm.Label5.width:=frm.ClientWidth-frm.Labels.left-5;

frm. Image1.Visible:=FALSE;

end;

// определение достигнутого уровня

procedure Itog(summa:integer;frmrTForml);

var

i:integer; buf:string;

begin

buf: = ";

str(summa:5,buf);

buf:='Результаты тестирования'+chr(13)

+'Всего баллов: '+buf; i:=1;

while (summa < level[i]) and (i<N_LEV) do

i:=i+l;

buf:=buf+chr(13)+mes[ i ] ; frm.Labels.caption:=buf;

end;

{$R *.DFM}

procedure TForm1.FormActivate(Sender: TObject);

begin

ResetForm(Form1);

if ParamCount = 0 then

begin

Labels.caption:= 'He задан файл вопросов теста.';

Button1.caption: ='Ok' ;

Button1.tag:=2; Button1.Enabled:=TRUE

end

else begin

fn := ParamStr(1); assignfile(f,fn); {$I-} reset(f);

{I+}

if IOResult=0 then begin

Info(f,Label5);

// прочитать и вывести информацию о тесте

GetLevel(f); // прочитать информацию об уровнях оценок

end;

end;

end;

procedure TForml.ButtonlClick(Sender: TObject); begin

case Button1.tag of 0: begin

Buttonl.caption:='Дальше';

Buttonl.tag:=1;

RadioButtonS.Checked:=TRUE; // вывод первого вопроса

Buttonl.Enabled:=False; ResetForm(Form1);

VoprosToScr(f,Forml,vopros}

end;

1: begin // вывод остальных вопросов

summa:=summa+score[otv];

RadioButtonS.Checked:=TRUE;

Button1.Enabled:=False;

ResetForm(Form1);

if not eof(f)

then VoprosToScr(f,Forml,vopros) else

begin

suima: =summa+score [otv] ;

closefile(f);

Buttonl.caption:='Ok';

Form1.caption: ='Результат';

Button1.tag:=2;

Button1.Enabled:=TRUE;

Itog(summa,Forml);

end;

end;

2: begin // завершение работы

Forml.Close;

end;

end;

end;

procedure TForm1.RadioButtonClick(Sender: TObject);

begin

if sender = RadioButtonl

then otv:=l

else if sender = RadioButtonl

then otv:=2

else if sender = RadioButton3

then otv:=3 else otv:=4;

Buttonl.enabled:=TRUE;

end;

end.

После запуска программы и вывода на экран стартовой формы происходит событие onActivate. Процедура FormActivate сначала вызывает процедуру ResetForm, которая, присваивая значение False свойству visible, делает невидимыми поля вывода альтернативных ответов и переключатели. Аналогично делается невидимой область иллюстрации. Кроме того, процедура устанавливает максимально возможную ширину полей меток альтернативных ответов.

После очистки формы проверяется, указан ли при запуске программы параметр — имя тестового файла.

Если параметр не указан (значение paramCount в этом случае равно нулю), то присвоением значения свойству caption метки Label5 выводится сообщение: Не задан файл вопросов теста И свойству Tag кнопки Button1 присваивается значение 2(Button1.Tag:=2;)

Если параметр задан, то открывается файл теста.

Программа тестирования получает имя файла теста как результат функции Paramstr(l). Реализация программы предполагает, что если имя файла теста задано без указания пути доступа к нему, то файл теста и файлы с иллюстрациями находятся в том же каталоге, что и программа тестирования. Если путь доступа указан, то файлы с иллюстрациями должны находиться в том же каталоге, что и файл теста. Такой подход позволяет сгруппировать все файлы одного теста в одном каталоге.

Открывается файл теста обычным образом. Сначала обращением к процедуре AssignFile имя файла связывается с файловой переменной, а затем вызывается инструкция открытия файла для чтения.

После успешного открытия файла вызывается процедура info, которая считывает из файла информацию о тесте и выводит ее присваиванием прочитанного текста свойству Caption поля метки Labels.

Затем вызывается процедура GetLevei, которая считывает из файла теста информацию об уровнях оценки. Эта процедура заполняет массивы level И mes.

После вывода информационного сообщения программа ждет, когда пользователь нажмет кнопку OK (Button1).

Командная кнопка Buttoni используется для:

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

После вывода информации о тесте значение свойства Tag кнопки Button: равно нулю. Поэтому в результате первого щелчка на кнопке Buttoni выполняется та часть программы, которая обеспечивает вывод первого вопроса, замену находящегося на кнопке текста ОК на текст Дальше, и устанавливает в выбранное состояние переключатель RadioButton5, который закрыт панелью и поэтому не виден пользователю. Кроме того, присваиванием значения False свойству Enabled кнопка Buttoni делается недоступной, тем самым блокируется переход к следующему вопросу до тех пор, пока не будет выбран один из ответов. Значению свойства Button1.Tag присваивается единица, тем самым выполняется подготовка к обработке следующего щелчка кнопки Button1.

После выбора ответа и нажатия кнопки Дальше (Buttoni) (в этом случае значение свойства Button1.Tag равно единице) к набранной сумме баллов добавляется количество баллов за выбранный ответ. Затем, если не достигнут конец файла, вызывается процедура вывода очередного вопроса. Если достигнут конец файла, то сначала закрывается файл теста, изменяется текст на кнопке Buttoni и значение Button1. Tag, а затем посредством процедуры Itog выводятся результаты тестирования.

Если значение Button1.Tag равно двум, то применением метода close к форме Form1 закрывается окно программы, в результате чего программа завершает работу.

Вывод вопроса и альтернативных ответов выполняет процедура VoprosToScr. Сначала процедура увеличивает счетчик вопросов vopros и присвоением значения свойству Caption формы выводит номер текущего вопроса в заголовок окна. Затем процедура читает строки из файла теста до тех пор, пока первым символом очередной прочитанной строки не будет точка или "обратная наклонная черта".

После вывода текста вопроса делается проверка: какой символ используется в качестве признака конца вопроса. Если обратная наклонная черта, что свидетельствует о том, что к вопросу есть иллюстрация, то свойству Form1.image1.Tag присваивается единица и из прочитанной строки выделяется имя файла иллюстрации.

Загрузка иллюстрации осуществляется применением метода LoadFromFile к свойству image1. Picture. Однако после загрузки иллюстрация на экране не появляется, так как значение свойства Image1. visible равно False.

После считывания иллюстрации процедура считывает вопросы. После обработки последнего вопроса, если была загружена иллюстрация, вызовом процедуры ScaleImage вычисляется и устанавливается размер области иллюстрации. После этого установкой значения свойства Imagel.Top задается положение верхней границы области иллюстрации, а присваиванием значения True свойству image1. visible иллюстрация делается видимой.

Так как количество символов в тексте вопроса и число альтернативных ответов от вопроса к вопросу могут меняться, и, следовательно, на экране они могут занимать разное количество строк, то каждый раз перед выводом текста очередного ответа устанавливается значение свойства тор как расстояние от нижней границы предыдущего альтернативного ответа. Для поля вывода первого альтернативного ответа (Label) значение тор вычисляется от нижней границы поля вопроса (Labels) или, если к вопросу есть иллюстрация, от нижней границы поля иллюстрации (imagei).

Выбор ответа пользователь осуществляет щелчком одного из переключателей. После вывода вопроса ни один из переключателей, соответствующих альтернативному ответу, не является выбранным. Выбран только переключатель RadioButtonS, который находится за панелью Panel1 и поэтому не виден пользователю.

Для обработки события onclick переключателей RadioButton1,

RadioButton2, RadioButton3 и RadioButton4

В Программе используется общая процедура— TForm1.RadioButtonciick. Эта процедура получает в качестве параметра объект, на котором произошло событие. Сравнивая полученное значение с именами объектов-кнопок выбора, процедура присваивает значение глобальной переменной otv, которая используется процедурой VoprosToScr для увеличения набранной суммы баллов. Кроме того, процедура TForm1.RadioButtonClick делает доступной кнопку перехода к следующему вопросу (Buttonl), которая после вывода очередного вопроса недоступна.

Процедура Itog, сравнивая набранную сумму баллов summa со значением элементов массива level, определяет, какого уровня достиг испытуемый, и выводит соответствующее сообщение присвоением значения свойству

Label5.Caption.

Усовершенствование программы


Очевидно, что приведенный выше текст программы был бы намного проше и изящней, если бы поля вывода альтернативных ответов и переключатели выбора ответов были бы объединены в массивы. Тогда программа могла бы обращаться к полям и переключателям не по имени, а по индексу.

Delphi позволяет объединить компоненты в массив, однако создаваться такие компоненты должны не во время создания формы приложения, а динамически — во время работы программы.

На рис. 15.7 приведен вид формы усовершенствованного приложения.

Рис. 15.7. Форма приложения Тест, версия 2

На форме отсутствуют поля вывода альтернативных ответов и переключатели выбора правильного ответа. Они будут созданы во время работы программы.

Объявление массива компонентов ничем не отличается от объявления обычного массива — указывается имя массива, диапазон изменения индекса и тип элементов массива. Ниже приведено объявление массивов компонентов формы разрабатываемой программы:

answer: array[1..N_ANSWERS] of TLabel;

// альтернативные ответы selector:

array[1..N_ANSWERS+1] of TRadioButton;

// кнопки выбора ответа

Однако, для того чтобы компонент появился в форме, одного объявления недостаточно. Компонент — это объект Delphi, и его объявление — это только указатель на область памяти, который без наличия объекта ни на что не указывает. Создается компонент применением метода Create к указателю на компонент, в нашем случае — к элементу массива.

Например, инструкции

answer[1] := TLabel.Create(self) ;

answer[1].Parent := Form1;

создают компонент Label и помещают его в форму.

После создания компонента программа должна выполнить его настройку, т. е. ту работу, которую во время создания формы приложения выполняет программист при помощи Object Inspector. Под настройкой понимается присваивание начальных значений тем свойствам компонента, предопределенные значения которых не отвечают предъявляемым требованиям.

Если компонент должен реагировать на некоторое событие, то. нужно написать процедуру обработки этого события и поместить объявление созданной процедуры в объявление типа формы. Например, объявление типа формы разрабатываемой программы должно выглядеть так:

type

TForm1 = class(TForm)

Label5: TLabel; // поле вывода вопроса

Image1: TImage; // область вывода иллюстрации

Panel1: TPanel;

Button1: TButton; // кнопка Ok, Дальше, Завершить

procedure FormActivate(Sender: TObject);

procedure FormCreate(Sender: TObject);

procedure ButtonlClick(Sender: TObject);

procedure SelectorClick(Sender: TObject);

private

{ Private declarations } public

{ Public declarations } end;

В отличие от других, сгенерированных Delphi, строк объявления типа, строка procedure SelectorClick(Sender: TObject) вставлена В объявление вручную.

Примечание

При создании процедуры обработки события для обычного компонента (компонента, который добавлен в форму во время разработки формы программы) Delphi автоматически генерирует заготовку процедуры обработки события и ее объявление. Программист должен написать только инструкции процедуры.

В случае создания процедуры обработки события для компонента, который создается динамически, программист должен полностью написать текст процедуры и поместить ее объявление в объявление формы.

После того как будет написана процедура обработки события, нужно связать эту процедуру с конкретным компонентом. Делается это путем присвоения имени процедуры обработки свойству, имя которого совпадает с именем обрабатываемого события. Например, инструкция

selector[1].OnClick : = SelectorClick;

задает процедуру обработки события Onclick для компонента selector [i]. В листинге 15.2 приведен полный текст программы Тест, версия 2.

Листинг 15.2. Программа тестирования, версия 2

unit test2_;

interface

uses

SysUtils, WinTypes, WinProcs,

Messages, Classes, Graphics,

Controls, Forms, Dialogs,

StdCtrls, ExtCtrls;

type

TForm1 = class(TForm)

Label5: TLabel; // поле вывода вопроса

Image1: TImage; // область вывода иллюстрации

Panel1: ТPanel; Button1: TButton;

// кнопка Ok, Дальше, Завершить

procedure FormActivate(Sender: TObject);

procedure FormCreate(Sender: TObject);

procedure ButtonlClick(Sender: TObject);

procedure SelectorClick(Sender: TObject);

private

{ Private declarations } public

{ Public declarations } end;

var

Form1: TForm1; // форма

implementation

const

N_ANSWERS=4; // четыре варианта ответов

N_LEVEL=4; // четыре уровня оценки

var

// динамически создаваемые компоненты

answer: array[1..N_ANSWERS] of TLabel;

// альтернативные ответы

selector: array[1..N_ANSWERS+1] of TRadioButton;

// кнопки выбора ответа

f:TextFile;

fn:string; // имя файла вопросов

level:array[1..N_LEVEL] of integer;

// сумма, соответствующая уровню

mes:array[1..N_LEVEL] of string;

// сообщение, соответствующее уровню

score:array[1..N_ANSWERS] of integer;

// очки за выбор ответа

summa:integer; // набрано очков

vopros:integer; // номер текущего вопроса

n_otv:integer; // число вариантов ответа

otv:integer; // номер выбранного ответа

// установка формы в исходное состояние

Procedure ResetForm(frm:TForm1);

var

i:integer; begin

for i:=1 to N_ANSWERS do begin

answer[i].width:=frm.ClientWidth-answer[i].left-5;

answer[i].Visible:=FALSE; Selector[i].Visible:=FALSE;

end;

frm. Label5.width:=frm.ClientWidth-frm.Label5.left-5;

frm. Image1.Visible:=False;

end;

// определение достигнутого уровня

procedure Itog(suirana:integer;frm:TForm1);

var

i:integer; buf:string;

begin buf: = ";

str(summa:5,buf); buf:='Результаты тестирования'+chr(13)

+'Всего баллов: '+buf; i:=1;

while (summa < level[i]) and (i<N_LEVEL) do

i:=i+l;

buf:=buf+chr(13)+mes[i];

frm.Labels.caption:=buf;

end;

procedure TForm1.FormCreate(Sender: TObject);

var

i: integer; begin

// создадим пять меток для вывода вопроса и альтернативных ответов

for i:=l to N_ANSWERS do

begin

answer[i]:=TLabel.Create(self);

answer[i].Parent:=Forml;

answer[i].Left:=36;

answer[i].Wordwrap:=True;

end;

// создадим переключатели для выбора ответа

for i:=l to N_ANSWERS+1 do

begin

selector[i]:=TRadioButton.Create(self);

selector[i].Parent:=self;

selector[i].Caption:='';

selector[i].Width:=17;

selector[i].Left:=16;

selector[i].Visible:=False;

selector[i].Enabled:=True;

selector[i].OnClick:=SelectorClick;

end;

ResetForm(Forml); end;

// вывод начальной информации о тесте

procedure info(var f:TextFile;l:TLabel);

var

s,buf:string; begin

buf:=''; repeat

readln(f,s); if s[l]<>'.'

then buf:=buf+s+' ';

until s[l] ='.';

Form1.Labels.caption:=buf;

end;

// прочитать информацию об оценках за тест

Procedure GetLevel(var f:TextFile);

var

i:integer; buf:string;

begin // заполняем значения глобальных массивов i:=1;

repeat

readln(f,buf); if buf[1] <> '.' then

begin mes[i]:=buf; readln(f,level[i]); i:=i+1;

end;

until buf[1]='.';

end;

// масштабирование иллюстрации

Procedure ScalePicture;

var

w,h:integer; // максимально допустимые размеры картинки

scaleX:real; // коэф. масштабирования по X

scaleY:real; // коэф. масштабирования по Y

scale:real; // общий коэф. масштабирования

i:integer; begin

// вычислить максимально допустимые размеры картинки

w:=Form1.ClientWidth-Form1.Labels.Left;

h:=Form1.ClientHeight

- Form1.Panel1.Height -5

- Form1.Label5.Top

- Forml.Label5.Height - 5;

for i:=1 to N_ANSWERS do

if answer[i].Caption <> ''

then h:=h-answer[i].Height-5;

// здесь определена максимально допустимая величина иллюстрации

// определить масштаб

if w>Form1.Image1.Picture.Width

then scaleX:=1

else scaleX:=w/Forml.Image1.Picture.Width;

if h>Forml.Image1.Picture.Height

then scaleY:=1

else scaleY:=h/Form1.Image1.Picture.Height;

if ScaleYOcaleX

then scale:=scaleY

else scale:=scaleX; // здесь масштаб определен

Form1.Image1.Top:=

Form1.Label5.Top+Forml.LabelS.Height+5;

Form1.Image1.Left:=Form1.Label5.Left;

Form1.Image1.Width:=

Round(Form1.Image1.Picture.Width*scale);

Form1.Image1.Height:=

Round(Form1.Image1.Picture.Height*scale)

Form1.Label5.Visible:=TRUE;

end;

// вывод вопроса на экран

Procedure VoprosToScr(var f:TextFile;

frm:TForm1;var vopros:integer),

var

i:integer; code:integer; s,buf:string;

ifn:string; // файл иллюстрации

begin

vopros:=vopros+1 ;

str(vopros:3,s);

frm. caption: ='Вопрос' + s;

// выведем текст вопроса

buf: = ";

repeat

readln(f, s) ;

if (s[l] <> '.') and (s[l] <> '\')

then buf:=buf+s+' ';

until (s[l] ='.'} or (s[l] = '\');

frm.Labels.caption:=buf;

if s[l] = '\'

then // к вопросу есть иллюстрация

begin

frm.Image1.Tag:=1; ifn:=copy(s,2,length(s));

try

frm.Image1.Picture.LoadFromFile(ifn); except

on E:EFOpenError do

frm.tag:=0; end //

try

end

else frm. Image1.Tag: =0;

// читаем варианты, ответов

for i:=1 to N_ANSWERS do begin

answer[i].caption:='';

answer[i].Width:=frm.ClientWidth-Form1.Label5.Left-5;

end; i:=l;

repeat

buf: = " ;

repeat // читаем текст варианта ответа

readln(f,s);

if (s[l]<>'.') and (s[1] <> ',')

then buf:=buf+s+' ';

until (s[1]=',')or(s[l]='.');

// прочитан альтернативный ответ

val (s[2],score[i],code);

answer[i].caption:=buf;

i:=i+l;

until s [1] = '.'; // здесь прочитана иллюстрация и альтернативные ответы

if Form1.Image1.Tag =1 // есть иллюстрация к вопросу?

then begin ScalePicture;

Forml.Image1.Visible:=TRUE;

end;

// вывод альтернативных ответов

i:=1;

while (answer[i].caption <> ") and (i <= N_ANSWERS) do

begin

if i = 1 then

if frm.Image1.Tag =1

then answer[1].top:=frm.Image1.Top+frm.Image1.Height+5

else answer[i].top:=frm.Label5.Top+frm.Label5.Height+5

else

answer [i] . top:=answer [i-1] .

top+ answer [i-1] . height+5;

selector[i] . top:=answer [i] .

top; selectorfi] ,visible:=TRUE;

answer [i] . visible : =TRUE; i:=i+l;

end;

end;

{$R *.DFM}

procedure TForml . FormActivate ( Sender : TOb j ect ) ;

begin

ResetForm ( Forml ) ;

if ParamCount = 0 then begin

Label3 . font . color : =clRed;

Label5. caption: = 'He задан файл вопросов теста.1;

Buttonl . caption : = ' Ok ' ; Buttonl.tag:=2;

Buttonl . Enabled : =TRUE

end else

begin

fn:=ParamStr (1) ;

assignf ile ( f , fn) ;

{$!-} reset (f) ;

if IOResult=0 then

begin

Inf <> (f, Label3) ;

GetLevel(f) ;

end;

summa:=0;

end;

end;

procedure TForm1. ButtonlClick (Sender: TObject)

begin

case Button1.tag of

0: begin

Button1.caption:='Дальше';

Buttonl.tag:=1;

Selector[N_ANSWERS+1].Checked:=TRUE; // вывод первого вопроса

Buttonl.Enabled:=False;

ResetForm(Forml);

VoprosToScr(f,Forml,vopros)

end;

1: begin // вывод остальных вопросов

summa:=summa+score[otv];

Selector[N_ANSWERS+1].Checked:=TRUE;

Button1.Enabled:=False; ResetForm(Form1);

if not eof(f)

then VoprosToScr(f,Forml,vopros) else

begin

closefile(f); Button1.caption:='Ok';

Forml.сарtiоn:='Результат';

Buttonl.tag:=2; Buttonl.Enabled:=TRUE;

Itog(summa,Form1);

end;

end;

2: begin // завершение работы

Form1.Close;

end;

end;

end;

// щелчок на кнопке выбора ответа

procedure TForml.SelectorClick(Sender: TObject);

var

i: integer;

begin

while selector[i].Checked = FALSE do

i:=i+l;

otv:=i;

Buttonl.enabled:=TRUE;

end;

end.

По сравнению с первым вариантом программа Тест, версия 2 обладает существенным преимуществом. Для ее модернизации, например для увеличения количества альтернативных ответов, достаточно изменить только описание именованной константы N_ANSWERS.


с операционной системой Windows, хорошо



Всем, кто работает с операционной системой Windows, хорошо знакома игра Сапер. В этом разделе рассматривается аналогичная программа — игра Сапер 2002.

Пример окна программы в конце игры, после того как игрок открыл клетку, в которой находится мина, приведен на рис. 15.8.



Рис. 15.8. Окно программы Сапер 2002

Правила


Игровое поле состоит из клеток, в каждой из которых может быть мина. Задача игрока — найти все мины и пометить их флажками.

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

Представление данных


В программе игровое поле представлено массивом N+2 на M+2, где N xM — размер игрового поля. Элементы массива с номерами строк от 1 до N и номерами столбцов от 1 до М соответствуют клеткам игрового поля (рис. 15.9), первые и последние столбцы и строки соответствуют границе игрового поля.

Рис. 15.9. Клетке игрового поля соответствует элемент массива

В начале игры каждый элемент массива, соответствующий клеткам игрового поля, может содержать число от 0 до 9. Ноль соответствует пустой клетке, рядом с которой нет мин. Клеткам, в которых нет мин, но рядом с которыми мины есть, соответствуют числа от 1 до 8. Элементы массива, соответствующие клеткам, в которых находятся мины, имеют значение 9.

Элементы массива, соответствующие границе поля, содержат -3.

В качестве примера на рис. 15.10 изображен массив, соответствующий состоянию поля в начале игры.

Рис. 15.10. Массив в начале игры

В процессе игры состояние игрового поля меняется (игрок открывает клетки и ставит флажки) и, соответственно, меняются значения элементов массива. Если игрок поставил в клетку флажок, то значение соответствующего элемента массива увеличивается на 100. Например, если флажок поставлен правильно в клетку, в которой есть мина, то значение соответствующего элемента массива станет 109. Если флажок поставлен ошибочно, например, в пустую клетку, элемент массива будет содержать число 100. Если игрок открыл клетку, то значение элемента массива увеличивается на 200. Такой способ кодирования позволяет сохранить информацию о исходном состоянии клетки.

Форма приложения


Главная (стартовая) форма игры Сапер 2002 приведена на рис. 15.11.

Рис. 15.11. Главная форма программы Сапер 2002

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

Основное окно программы содержит компонент MainMenu1, который представляет собой главное меню программы. Значок компонента MainMenu находится на вкладке Standard (рис. 15.12).

Рис. 15.12. Компонент MainMenu

Значок компонента MainMenu можно поместить в любое место формы, так как во время работы программы он не виден. Пункты меню появляются в верхней части формы в результате настройки меню. Для настройки меню используется редактор меню, который запускается двойным щелчком левой кнопкой мыши на значке компонента или выбором из контекстного меню компонента команды Menu Designer. В начале работы над новым меню, сразу после добавления компонента к форме, в окне редактора находится один-единственный прямоугольник — заготовка пункта меню. Чтобы превратить эту заготовку в меню, нужно в окне Object Inspector в поле Caption ввести название меню.

Если перед какой-либо буквой в названии меню ввести знак &, то во время работы программы можно будет активизировать этот пункт меню путем нажатия комбинации клавиши <Аlt> и клавиши, соответствующей символу, перед которым стоит знак &. В названии меню эта буква будет подчеркнута.

Чтобы добавить в главное меню элемент, необходимо в окне редактора меню выбрать последний (пустой) элемент меню и ввести название нового пункта.

Чтобы добавить в меню команду, необходимо выбрать пункт меню, в который нужно добавить команду, переместить указатель активного элемента меню в конец списка команд меню и ввести название команды.

На рис. 15.13 приведено окно редактора меню, в котором находится меню программы Сапер 2002.

После того как будет сформирована структура меню, нужно, используя окно Object Inspector, выполнить настройку элементов меню (выбрать настраиваемый пункт меню можно в окне формы приложения или из списка объектов в верхней части окна Object Inspector). Каждый элемент меню (пункты и команды) — это объект типа TMenuitem. Свойства объектов TMenuitem (табл. 15.7) определяют вид меню во время работы программы.

Рис. 15.13. Структура меню программы Сапер 2002

Таблица 15.7. Свойства объекта TMenuItem

Свойство

Определяет

Name

Имя элемента меню. Используется для доступа к свойствам

Caption

Название меню или команды

Bitmap

Значок, который отображается слева от названия элемента меню

Enabled

Признак доступности элемента меню. Если значение свойства равно False, то элемент меню недоступен, при этом название меню отображается серым цветом

При выборе во время работы программы элемента меню происходит событие Click. Чтобы создать процедуру обработки этого события, нужно в окне формы выбрать пункт меню и щелкнуть левой кнопкой мыши - Delphi создаст шаблон процедуры обработки этого события. В качестве примера ниже приведена процедура обработки события, которое возникает в результате выбора из меню ? команды Справка. N3 — это имя элемента меню, соответствующего этой команде.


Начало игры


В начале игры нужно расставить мины, затем для каждой клетки поля подсчитать, сколько мин находится в соседних клетках. Процедура NewGame (ее текст приведен в листинге 15.3) решает эту задачу.

Листинг 15.3. Процедура NewGame

// новая игра — генерирует новое поле

procedure NewGame ();

row,col : integer; // координаты клетки (индексы массива)

n : integer; // количество поставленных мин

k : integer; // кол-во мин в соседних клетках

begin

// Очистим эл-ты массива, соответствующие клеткам

// игрового поля

for row :=1 to MR do

for col :=1 to MC do Pole[row,col] := 0;

// расставим мины

Randomize О; // инициализация ГСЧ

n := 0; // кол-во мин

repeat

row := Random(MR) + 1;

col := Random(MC) + 1;

if (Pole[row,col] <> 9) then begin

Pole[row,col] := 9; n := n+1;

end;

until (n = NM);

// для каждой клетки вычислим // кол-во мин в соседних клетках

for row := 1 to MR do

for col := 1 to MC do

if (Pole[row,col] <> 9) then

begin k :=0 ;

if Pole[row-l,col-l] = 9 then k = k + 1;

if Pole[row-1,col] =' 9 then k = k + 1;

if Pole[row-1,col+1] = 9 then k = k + 1;

if Pole[row,col-1] = 9 then k = k + 1;

if Pole[row,col+1] = 9 then k = k + 1;

if Pole[row+1,col-1] = 9 then k = k + 1;

if Pole[row+1,col] = 9 then k = k + 1;

if Pole[row+l,col+l] = 9 then k := k + 1;

Pole[row,col] := k;

end;

status := 0; // начало игры

nMin := 0; // нет обнаруженных мин

nFlag := 0; // нет поставленных флагов

end;

После того как процедура NewGame расставит мины, процедура showPoie (ее текст приведен в листинге 15.4) выводит изображение игрового поля.

Листинг 15.4. Процедура ShowPole

// Показывает поле

Procedure ShowPoie(Canvas : TCanvas; status : integer);

var

row,col : integer;

begin

for row := 1 to MR do

for col := 1 to MC do

Kletka(Canvas, row, col, status);

end;

Процедура showPoie выводит изображение поля последовательно, клетка за клеткой. Вывод изображения отдельной клетки выполняет процедура Kletka, ее текст приведен в листинге 15.5. Процедура Kletka используется для вывода изображения поля в начале игры, во время игры и в ее конце. В начале игры (значение параметра status = 0) процедура выводит только контур клетки, во время игры — количество мин в соседних клетках или флажок, а в конце отображает исходное состояние клетки и действия пользователя. Информацию о фазе игры процедура Kletka получает через параметр status.

Листинг 15.5. Процедура Kletka

// выводит на экран изображение клетки

Procedure Kletka(Canvas : TCanvas;

row, col, status : integer);

var

x,y : integer; // координаты области вывода

begin

x := (col-1)* W + I; у := (row-1)* H + 1;

if status = 0 then begin

Canvas.Brush.Color := clLtGray;

Canvas.Rectangle(x-1,y-1,x+W,y+H);

exit;

end;

if Pole[row,col] < 100 then

begin

Canvas.Brush.Color := clLtGray; // неоткрытые — серые

Canvas.Rectangle(x-1,y-l,x+W,y+H);

// если игра завершена (status = 2), то показать мины

if (status = 2) and (Pole[row,col] = 9)

then Mina(Canvas, x, y) ; exit; end;

// открытая клетка

Canvas.Brush.Color := clWhite; // открытые белые

Canvas.Rectangle(x-1,y-l,x+W,y+H);

if (Pole[row,col] = 100)

then exit; // клетка открыта, но она пустая

if (Pole[row,col] >= 101)

and (Pole[row,col] <= 108)

then begin // в соседних клетках есть мины

Canvas.Font.Size := 14;

Canvas.Font.Color := clBlue;

Canvas.TextOut(x+3,y+2,IntToStr(Pole[row,col] -100));

exit;

end;

if (Pole[row,col] >= 200)

then Flag(Canvas, x, y);

if (Pole[row,col] = 109)

then // на этой мине подорвались!

begin

Canvas.Brush.Color := clRed;

Canvas.Rectangle(x-1,y-1,x+W,y+H);

end;

if ((Pole[row,col] mod 10) = 9)

and (status = 2) then

Mina(Canvas, x, y);

end;

Игра


Во время игры программа воспринимает нажатия кнопок мыши и, в соответствии с правилами игры, открывает клетки или ставит в клетки флажки.

Основную работу выполняет процедура обработки события onMouseDown (ее текст приведен в листинге 15.6). Сначала процедура преобразует координаты точки, в которой игрок нажал кнопку мыши, в координаты клетки игрового поля. Затем делает необходимые изменения в массиве Pole и, если нажата правая кнопка, рисует в клетке флажок. Если нажата левая кнопка в клетке, в которой нет мины, то эта клетка открывается, на экран выводится ее содержимое. Если нажата левая кнопка в клетке, в которой есть мина, то вызывается процедура showPole, которая показывает все мины, в том числе и те, которые игрок не успел найти.

Листинг 15.6. Обработка события OnMouseDown на поверхности игрового поля

// нажатие кнопки мыши на игровом поле

procedure TForm1.Form1MouseDown(Sender: TObject;

Button: TMouseButton;

Shift: TShiftState; X, Y: Integer);

var

row, col : integer;

begin

if status = 2 // игра завершена

then exit;

if status = 0 then // первый щелчок

status := 1;

// преобразуем координаты мыши в индексы

// клетки поля

row := Trunc(y/H) + 1;

col := Trunc(x/W) + 1;

if Button = mbLeft then

begin

if Pole[row,col] = 9 then

begin // открыта клетка, в которой есть мина

Pole[row,col] := Pole[row,col] + 100;

status := 2; // игра закончена

ShowPole(Form1.Canvas, status);

end else

if Pole[row,col] < 9 then

Open(row,col);

end else

if Button = mbRight then

if Pole[row,col] > 200 then begin

// уберем флаг и закроем клетку

nFlag := nFlag — 1;

Pole[row,col] := Pole[row,col]-200;

// уберем флаг

x := (col-D* W + 1; у := (row-1)* H + 1;

Canvas.Brush.Color := clLtGray;

Canvas.Rectangle(x-1,y-1,x+W,y+H);

end else

begin // поставить в клетку флаг

nFlag := nFlag + 1; if Pole[row,col] = 9

then nMin := nMin + 1;

Pole[row,col]:=Pole[row,col]+200;

// поставили флаг

if (nMin = NM) and (nFlag = NM) then begin

status := 2;

// игра закончена

ShowPole(Form1.Canvas, status);

end

else Kletka(Form1.Canvas, row, col, status);

end;

end;

Справочная информация


При выборе из меню ? команды Справка появляется справочная информация — правила игры (рис. 15.14).

Рис. 15.14. Окно справочной информации

Процесс создания СНМ-файла подробно описан в гл. 14. Процедура, обеспечивающая вывод справочной информации, приведена в листинге 15.7.

Примечание

Перед непосредственным созданием процедуры, обеспечивающей вывод справочной информации, в главную форму необходимо добавить компонент HhOpen.

Листинг 15.7. Вывод справочной информации

// выбор из меню ? команды Справка

procedure TForm1.N3Click(Sender: TObject);

HelpFile : string; // файл справки

HelpTopic : string; // раздел справки

pwHelpFile : PWideChar;

// файл справки (указатель на строку WideChar)

pwHelpTopic : PWideChar;

// раздел (указатель на строку WideChar)

begin

HelpFile := 'saper.chm';

HelpTopic := 'saper_02.htm';

// выделить память для

WideChar-строк GetMem(pwHelpFile, Length(HelpFile) * 2);

GetMem(pwHelpTopic, Length(HelpTopic)*2);

// преобразовать ANSI-строку в WideString-строку

pwHelpFile := StringToWideChar(HelpFile,

pwHelpFile, MAX_PATH*2);

pwHelpTopic := StringToWideChar(HelpTopic,

pwHelpTopic,32);

// вывести справочную информацию

Form1.Hhopen1.OpenHelp(pwHelpFile,

pwHelpTopic);

end;


Информация о программе


При выборе из меню ? команды О программе на экране должно появиться одноименное окно (рис. 15.15).

Рис. 15.15. Окно О программе

Чтобы программа во время своей работы могла вывести на экран окно, отличное от главного (стартового), нужно создать это окно. Делается это выбором из меню File команды New form. В результате выполнения команды New form в проект добавляется новая форма и соответствующим ей модуль.

Вид формы AboutForm после добавления необходимых компонентов приведен на рис. 15.16, значения ее свойств — в табл. 15.8.

Рис. 15.16. Форма О программе

Таблица 15.8. Значения свойств формы О программе

Свойство

Значение

Name

AboutForm

Caption

0 программе

BorderStyle

BsSingle

Borderlcons . biSystemMenu

False

Borderlcons .biMininize

False

Borderlcons . biMaximize

False

Вывод окна О программе выполняет процедура обработки события click, которое происходит в результате выбора из меню ? команды О программе.

Непосредственно вывод окна выполняет метод showModai, который выводит окно как модальный диалог.

Листинг 15.8. Вывод окна О программе

// выбор из меню ? команды О программе

procedure TForm1.N4Click(Sender: TObject);

begin

AboutForm.Top :=

Trunc(Forml.Top + Forml.Height/2 - AboutForm.Height/2);

AboutForm.Left :=

Trunc (Form1.Left +Form1 .Width/2

- AboutForm. Width/2 }; AboutForm. ShowModal;

end;

Примечание

Модальный диалог перехватывает все события, адресованные другим окнам приложения. Пока модальный диалог находится на экране, другие окна приложения не реагируют на действия пользователя. Для продолжения работы с приложением нужно закрыть модальный диалог. В большинстве программ в том числе и в Delphi, информация о программе реализована как модальный диалог.

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

procedure TAboutForm.ButtonlClick (Sender: TObject) ;

begin

ModalResult := mrOk;

end;


Листинги


Полный текст программы Сапер 2002 представлен ниже. В листинге 15.9 приведен модуль, соответствующий главной форме, В листинге 15.10 -форме О программе.

Листинг 15.9. Модуль главного окна программы Сапер 2002

unit saper_1;
interface

uses
Windows, Messages, SysUtils, Classes,

Graphics, Controls, Forms, Dialogs,
Menus, StdCtrls, OleCtrls, HHOPENLib_TLB;

type
TForm1 = class(TForm)
MainMenu1: TMainMenu;
N1: TMenuItem;
N2: TMenuItem;
N3: TMenuItem;
N4: TMenuItem;
Hhopen1: THhopen;

procedure Form1Create(Sender: TObject);
procedure Form1Paint(Sender: TObject);
procedure Form1MouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure N1Click(Sender: TObject);

procedure N4Click(Sender: TObject);
procedure N3Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;

var
Form1: TForm1;


implementation

uses saper_2;

{$R *.DFM}

const
MR = 10; // кол-во клеток по вертикали
MC = 10; // кол-во клеток по горизонтали
NM = 10; // кол-во мин

W = 40; // ширина клетки поля
H = 40; // высота клетки поля

var
Pole: array[0..MR+1, 0.. MC+1] of integer; // минное поле
// значение элемента массива:
// 0..8 - количество мин в соседних клетках
// 9 - в клетке мина
// 100..109 - клетка открыта
// 200..209 - в клетку поставлен флаг

nMin : integer; // кол-во найденных мин
nFlag : integer; // кол-во поставленных флагов

status : integer; // 0 - начало игры; 1- игра; 2 -результат


Procedure NewGame(); forward;

// генерирует новое поле
Procedure ShowPole(Canvas : TCanvas; status : integer); forward;// Показывает поле
Procedure Kletka(Canvas : TCanvas; row, col, status : integer); forward; // выводит содержимое клетки
Procedure Open( row, col : integer); forward;// открывает текущую и все соседние клетки, в которых нет мин
Procedure Mina(Canvas : TCanvas; x, y : integer); forward; // Рисует мину
Procedure Flag( Canvas : TCanvas; x, y : integer); forward;// Рисует флаг

// выводит на экран содержимое клетки
Procedure Kletka(Canvas : TCanvas; row, col, status : integer);
var
x,y : integer; // координаты области вывода
begin
x := (col-1)* W + 1;
y := (row-1)* H + 1;

if status = 0 then
begin
Canvas.Brush.Color := clLtGray;
Canvas.Rectangle(x-1,y-1,x+W,y+H);
exit;
end;

if Pole[row,col] < 100 then
begin
Canvas.Brush.Color := clLtGray; // не открытые - серые
Canvas.Rectangle(x-1,y-1,x+W,y+H);
// если игра завершена (status = 2), то показать мины
if (status = 2) and (Pole[row,col] = 9)
then Mina(Canvas, x, y);
exit;
end;

// открываем клетку
Canvas.Brush.Color := clWhite; // открытые белые
Canvas.Rectangle(x-1,y-1,x+W,y+H);
if ( Pole[row,col] = 100 )
then exit; // клетка открыта, но она пустая

if ( Pole[row,col] >= 101) and (Pole[row,col] <= 108 ) then
begin
Canvas.Font.Size := 14;
Canvas.Font.Color := clBlue;
Canvas.TextOut(x+3,y+2,IntToStr(Pole[row,col] -100 ));
exit;
end;

if ( Pole[row,col] >= 200 ) then
Flag(Canvas, x, y);

if (Pole[row,col] = 109 ) then // на этой мине подорвались!
begin
Canvas.Brush.Color := clRed;
Canvas.Rectangle(x-1,y-1,x+W,y+H);
end;

if ( (Pole[row,col] mod 10) = 9) and (status = 2) then
Mina(Canvas, x, y);
end;

// Показывает поле
Procedure ShowPole(Canvas : TCanvas; status : integer);
var
row,col : integer;
begin
for row := 1 to MR do
for col := 1 to MC do
Kletka(Canvas, row, col, status);
end;

// рекурсивная функция открывает текущую и все соседние
// клетки, в которых нет мин
Procedure Open( row, col : integer);
begin
if Pole[row,col] = 0 then
begin
Pole[row,col] := 100;
Kletka(Form1.Canvas, row,col, 1);
Open(row,col-1);
Open(row-1,col);
Open(row,col+1);
Open(row+1,col);
//примыкающие диагонально
Open(row-1,col-1);
Open(row-1,col+1);
Open(row+1,col-1);
Open(row+1,col+1);
end
else
if (Pole[row,col] < 100) and ( Pole[row,col] <> -3 ) then
begin
Pole[row,col] := Pole[row,col] + 100;
Kletka(Form1.Canvas, row, col, 1);
end;
end;

// новая игра - генерирует новое поле
procedure NewGame();

var
row,col : integer; // координаты клетки
n : integer; // количество поставленных мин
k : integer; // кол-во мин в соседних клетках
begin
// Очистим эл-ты массива, соответствующие клеткам
// игрового поля.
for row :=1 to MR do
for col :=1 to MC do
Pole[row,col] := 0;

// расставим мины
Randomize(); // инициализация ГСЧ
n := 0; // кол-во мин
repeat
row := Random(MR) + 1;
col := Random(MC) + 1;
if ( Pole[row,col] <> 9) then
begin
Pole[row,col] := 9;
n := n+1;
end;
until ( n = NM );

// для каждой клетки вычислим
// кол-во мин в соседних клетках
for row := 1 to MR do
for col := 1 to MC do
if ( Pole[row,col] <> 9 ) then
begin
k :=0 ;
if Pole[row-1,col-1] = 9 then k := k + 1;
if Pole[row-1,col] = 9 then k := k + 1;
if Pole[row-1,col+1] = 9 then k := k + 1;
if Pole[row,col-1] = 9 then k := k + 1;
if Pole[row,col+1] = 9 then k := k + 1;
if Pole[row+1,col-1] = 9 then k := k + 1;
if Pole[row+1,col] = 9 then k := k + 1;
if Pole[row+1,col+1] = 9 then k := k + 1;
Pole[row,col] := k;
end;
status := 0; // начало игры
nMin := 0; // нет обнаруженных мин
nFlag := 0; // нет флагов

end;

// Рисует мину
Procedure Mina(Canvas : TCanvas; x, y : integer);
begin
with Canvas do
begin
Brush.Color := clGreen;
Pen.Color := clBlack;
Rectangle(x+16,y+26,x+24,y+30);
Rectangle(x+8,y+30,x+16,y+34);
Rectangle(x+24,y+30,x+32,y+34);
Pie(x+6,y+28,x+34,y+44,x+34,y+36,x+6,y+36);

MoveTo(x+12,y+32); LineTo(x+26,y+32);
MoveTo(x+8,y+36); LineTo(x+32,y+36);
MoveTo(x+20,y+22); LineTo(x+20,y+26);
MoveTo(x+8, y+30); LineTo(x+6,y+28);
MoveTo(x+32,y+30); LineTo(x+34,y+28);
end;
end;

// Рисует флаг
Procedure Flag( Canvas : TCanvas; x, y : integer);
var
p : array [0..3] of TPoint; // координаты флажка и нижней точки древка
m : array [0..4] of TPoint; // буква М
begin
// зададим координаты точек флажка
p[0].x:=x+4; p[0].y:=y+4;
p[1].x:=x+30; p[1].y:=y+12;
p[2].x:=x+4; p[2].y:=y+20;
p[3].x:=x+4; p[3].y:=y+36; // нижняя точка древка

m[0].x:=x+8; m[0].y:=y+14;
m[1].x:=x+8; m[1].y:=y+8;
m[2].x:=x+10; m[2].y:=y+10;
m[3].x:=x+12; m[3].y:=y+8;
m[4].x:=x+12; m[4].y:=y+14;

with Canvas do
begin
// установим цвет кисти и карандаша
Brush.Color := clRed;
Pen.Color := clRed;

Polygon(p); // флажок

// древко
Pen.Color := clBlack;
MoveTo(p[0].x, p[0].y);
LineTo(p[3].x, p[3].y);

// буква М
Pen.Color := clWhite;
Polyline(m);

Pen.Color := clBlack;
end;
end;

// выбор из меню ? команды О программе
procedure TForm1.N4Click(Sender: TObject);
begin
AboutForm.Top := Trunc(Form1.Top + Form1.Height/2 - AboutForm.Height/2);
AboutForm.Left := Trunc(Form1.Left +Form1.Width/2 - AboutForm.Width/2);
AboutForm.ShowModal;
end;

procedure TForm1.Form1Create(Sender: TObject);
var
row,col : integer;
begin
// В неотображаемые эл-ты массива, которые соответствуют
// клеткам по границе игрового поля запишем число -3.
// Это значение используется функцией Open для завершения
// рекурсивного процесса открытия соседних пустых клеток.
for row :=0 to MR+1 do
for col :=0 to MC+1 do
Pole[row,col] := -3;

NewGame(); // "разбросать" мины
Form1.ClientHeight := H*MR + 1;
Form1.ClientWidth := W*MC + 1;
end;


// нажатие кнопки мыши на игровом поле
procedure TForm1.Form1MouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
var
row, col : integer;
begin
if status = 2 // игра завершена
then exit;

if status = 0 then // первый щелчок
status := 1;

// преобразуем координаты мыши в индексы
// клетки поля
row := Trunc(y/H) + 1;
col := Trunc(x/W) + 1;

if Button = mbLeft then
begin
if Pole[row,col] = 9 then
begin // открыта клетка, в которой есть мина
Pole[row,col] := Pole[row,col] + 100;
status := 2; // игра закончена
ShowPole(Form1.Canvas, status);
end
else if Pole[row,col] < 9 then
Open(row,col);
end
else
if Button = mbRight then
if Pole[row,col] > 200 then
begin
// уберем флаг и закроем клетку
nFlag := nFlag - 1;
Pole[row,col] := Pole[row,col] - 200; // уберем флаг
x := (col-1)* W + 1;
y := (row-1)* H + 1;
Canvas.Brush.Color := clLtGray;
Canvas.Rectangle(x-1,y-1,x+W,y+H);
end
else
begin // поставить в клетку флаг
nFlag := nFlag + 1;
if Pole[row,col] = 9
then nMin := nMin + 1;
Pole[row,col] := Pole[row,col]+ 200; // поставили флаг
if (nMin = NM) and (nFlag = NM) then
begin
status := 2; // игра закончена
ShowPole(Form1.Canvas, status);
end
else Kletka(Form1.Canvas, row, col, status);
end;
end;

// Выбор меню Новая игра
procedure TForm1.N1Click(Sender: TObject);
begin
NewGame();
ShowPole(Form1.Canvas,status);
end;

// выбор из меню ? команды Справка
procedure TForm1.N3Click(Sender: TObject);

var
HelpFile : string; // файл справки
HelpTopic : string; // раздел справки
pwHelpFile : PWideChar; // файл справки (указатель на WideChar строку)
pwHelpTopic : PWideChar; // раздел (указатель на WideChar строку)
begin
HelpFile := 'saper.chm';
HelpTopic := 'saper_02.htm';

// выделить память для WideChar строк
GetMem(pwHelpFile, Length(HelpFile) * 2);
GetMem(pwHelpTopic, Length(HelpTopic)*2);

// преобразовать Ansi строку в WideString строку
pwHelpFile := StringToWideChar(HelpFile,pwHelpFile,MAX_PATH*2);
pwHelpTopic := StringToWideChar(HelpTopic,pwHelpTopic,32);

// вывести справочную информацию
Form1.Hhopen1.OpenHelp(pwHelpFile,pwHelpTopic);

end;

procedure TForm1.Form1Paint(Sender: TObject);
begin
ShowPole(Form1.Canvas, status);
end;
end.