Система тестирования. Часть 7
В предыдущей части цикла статей была выполнена подготовительная часть для написания программы-теста. Сегодня мы приступим, непосредственно, к разработке самого теста.
Для начала, нужно создать новый проект в папке тест, и подключить к нему все файлы из lib.
Интерфейс программы
Рассмотрим внешний вид приложения-теста. В отличие, от программы для создания теста, интерфейс будет здесь более простым. Нужны лишь две метки для отображения названия теста и вопроса, так же нужен блок вариантов ответа и три кнопки: «Назад», «Далее» и «Закончить».
Исходя из вышесказанного, приложение может выглядеть так (скачать dfm):
Основные компоненты:
- TNameLabel (TLabel) – метка, где отображается название теста
- QTextLabel (TLabel) – метка, где отображается текста вопроса
- APanel (TPanel) – блок варинатов ответа
- PrevButton (TButton)– кнопка «Назад»
- NextButton (TButton)– кнопка «Далее»
- ResultButton (TButton) – кнопка «Закончить»
- QCountLabel (TLabel) – метка, где отображается номер текущего вопроса и их общее количество
Выбор файла теста и получение данных
Необходимо, что бы файл теста можно было выбрать перед загрузкой самой программы. Это несколько обезопасит тест, и облегчит работу с ним. Существует два варианта как это реализовать:
- Передавать имя файла как параметр командной строки
- Перед созданием формы, отображать диалоговое окно выбора файла
Мы рассмотрим оба варианта.
Для начала, в основной класс формы добавим следующие поля:
- Data (TStData) – объект, для работы с данными.
- Questions (TObjectList) – список вопросов(TStQuestionTest)
- Answers(TObjectList) – список блоков вариантов ответа (TStAnswerBlock) для текущего вопроса
Так же, добавим привычные уже методы Load, LoadQuestion и LoadData.
- TMainForm = class(TForm)
- //...
- private
- procedure Load;
- procedure LoadQuestion;
- procedure LoadData;
- public
- Data: TStData;
- Questions: TObjectList;
- Answers: TObjectList;
- end;
Метод Load
Тот самый метод, отвечающий за выбор файла теста и получение из него данных. Определяет, передано ли имя файла как параметр командной строки, и если нет, то показывает диалоговое окно выбора файла. Если файл не выбран, или возникли ошибки при чтении, то программа закрывается. В случае успеха вызывает метод LoadData.
- procedure TMainForm.Load;
- var
- fileName: string;
- begin
- try
- // Если имя файла передано как параметр ком. строки
- if ParamCount > 0 then fileName := ParamStr(1)
- // В противном случае, показывает диалоговое окно
- else if OpenDialog.Execute then fileName := OpenDialog.FileName
- else Close; // Если ничено не выбранно, то закрываем форму
- Data.Load( fileName ); // Получаем данные
- LoadData; // Загружаем данные в форму
- except
- Close; // Если возникли ошибки, закрываем форму
- end;
- end;
Метод LoadData
Отображает название теста и вызывает метод LoadQuestion.
- procedure TMainForm.LoadData;
- begin
- TNameLabel.Caption := Data.StTest.Text;
- LoadQuestion;
- end;
Метод LoadQuestion
Метод, который получает активные вопросы из файла, и если необходимо, то работает лишь с заданным количество вопросов.
- procedure TMainForm.LoadQuestion;
- var
- tmpQuestion: TStQuestion;
- tmpQuestions: TObjectList;
- i, max: integer;
- begin
- Questions.Clear; // Очищаем список вопросов
- // Создаем врменные список для хранения активных
- // вопросов
- tmpQuestions := TObjectList.Create;
- // Пробегаем циклом по всем вопросам
- for i := 0 to Data.StQuestionList.Count - 1 do
- begin
- // Получаем текущий вопрос
- tmpQuestion := ( TStQuestionTest( Data.StQuestionList.Items[i] ) );
- // Если он активный, тогда добавляем в временный список
- if tmpQuestion.Active then
- tmpQuestions.Add( tmpQuestion );
- end;
- // Если количество запросов, которые нужно отображать заданы
- // и меньше количества активных вопросов, то отмечаемы это
- if (Data.StTest.QCount > 0) and
- (Data.StTest.QCount <= tmpQuestions.Count) then
- max = Data.StTest.QCount;
- else // В противном случаем, макс. кол-во вопросов равно кол-ву активных
- max = tmpQuestions.Count;
- // Копируем заданное кол-во вопросов из временного списка в основной
- for i := 0 to max - 1 do
- Questions.Add( TStQuestionTest (tmpQuestions.Items[i]) );
- end;
Перемешивание вопросов и вариантов ответа
В требования к системе, было оговорено, что бы вопросы и варианты ответа выводились в случайном порядке. То есть, нам нужно просто случайно отсортировать списки Questionsи Answers.
Для этого достаточно использовать метод Sort класса TObjectList. Он сортирует список по критерию, устанавливаемому принимаемой функцией. Которая сравнивает два элемента списка и возвращает отрицательное число, если первый меньше второго, 0 – если они равный, и положительное число, если второй больше первого.
Для случайной сортировки, достаточно, что бы функция рандомно возвращала число от -1 до 1:
- function RandomList(Iteml, Item2: Pointer): Integer;
- begin
- Result := Random( 3 ) - 1;
- end;
Текущий вопрос
Добавим свойство QIndex, для обозначения номера вопроса, на который в данный момент пытается ответить тестируемый. Так же, добавим скрытое поле SelectQuestion типа TStQuestionTest, в котором будет сам текущий вопрос.
- TMainForm = class(TForm)
- //...
- private
- FQIndex: Integer;
- SelectQuestion: TStQuestionTest;
- //...
- procedure SetQIndex(const Value: Integer);
- procedure UpdateCount;
- procedure ShowAnswers;
- public
- //...
- property QIndex: Integer read FQIndex write SetQIndex;
- end;
Метод SetQindex – запись в свойство QIndex
В общем-то, основной метод этой программы. Для начала, определяется, влезает ли желаемый индекс вопроса в интервал от 0 до количества самих вопроса. Если да, то в поле SelectQuestion записывается вопрос под заданным индексом. Варианты ответа для него сортируются в случайном порядке. Далее отображается текст вопроса, и вызываются методы ShowAnswers – отображение вариантов ответа, и UpdateCount – обновления счетчика в метке QCountLabel.
- procedure TMainForm.SetQIndex(const Value: Integer);
- begin
- if (Value >= 0) and (Value < Questions.Count) then
- begin
- // Получаем текущий вопрос
- SelectQuestion := TStQuestionTest ( Questions.Items[ Value ] );
- // Разбрасываем варинты ответа в случайном порядке
- SelectQuestion.StAnswerList.Sort( RandomList );
- // Отображаем текст вопроса
- QTextLabel.Caption := SelectQuestion.Text;
- // Показываем варинты ответа
- ShowAnswers;
- FQIndex := Value;
- // Обновляем счетчик
- UpdateCount;
- end;
- end;
Метод ShowAnswers
Метод, который отображает варианты ответа как блоки типа TStAnswerBlock (см. предыдущую часть) и так же, заполняет список Answers.
- procedure TMainForm.ShowAnswers;
- var
- i: integer;
- ABlock: TStAnswerBlock;
- begin
- Answers.Clear;
- // Проходим по всем вариантам ответа для текущего вопроса
- for i := 0 to SelectQuestion.StAnswerList.Count - 1 do
- begin
- // Создаем блок варинта ответа
- ABlock := TStAnswerBlock.Create( TStAnswerTest(
- SelectQuestion.StAnswerList.Items[i]
- ),
- APanel, tmpImage.Picture );
- ABlock.Left := 10; // Отступ слева
- ABlock.Top := i*30 + 10; // Отступ сверху
- Answers.Add( ABlock ); // Добавляем блок в список
- end;
- end;
Метод UpdateCount
Простой метод, который в метке QCountLabel выводит номер текущего вопроса и их общее количество.
- procedure TMainForm.UpdateCount;
- begin
- QCountLabel.Caption := 'Вопрос ' + IntToStr( FQIndex + 1 )
- + ' из ' + IntToStr( Questions.Count );
- end;
Создание формы
Рассмотрим обработчик события OnCreate для формы. Тут, в общем, все просто. Создаются списки, вызывается метод Load, рандомно сортируются вопросы и текущим задается первый вопрос.
- procedure TMainForm.FormCreate(Sender: TObject);
- begin
- Data := TStData.GetInstance;
- Questions := TObjectList.Create;
- Answers := TObjectList.Create;
- Load;
- Questions.Sort( RandomList );
- QIndex := 0;
- end;
Обработчик события OnActivate служит дополнительной проверкой. Если название файла не передано, то закрывает приложение.
- procedure TMainForm.FormActivate(Sender: TObject);
- begin
- if Data.FileName = '' then
- Close;
- end;
Кнопки «Далее» и «Назад»
В обработчиках события клика для этих кнопок нет ничего сложного. Просто увеличение или уменьшение QIndex на единицу.
- procedure TMainForm.NextButtonClick(Sender: TObject);
- begin
- QIndex := QIndex + 1;
- end;
- procedure TMainForm.PrevButtonClick(Sender: TObject);
- begin
- QIndex := QIndex - 1;
- end;
Проверка и отображение результатов
Для отображения результатов создадим отдельную форму (диалог): ResultForm (скачать dfm). Ее примерный внешний вид:

Переопределим конструктор этой формы, добавив параметр SetResult – собственно, результат тестирования.
- constructor TResultForm.Create(AOwner: TComponent; SetResult: integer);
- begin
- inherited Create( AOwner );
- RightPercentLabel.Caption := IntToStr( SetResult ) + '%';
- end;
Метод Check
Вернемся к главной форме. Сначала добавим метод Check. Он будет проверять все вопросы и выставлять средний бал:
- function TMainForm.Check: Integer;
- var
- i: integer;
- totalResult: integer;
- begin
- totalResult := 0; // Общий процент верных ответов
- // Цикл по вопросам, и определение процента
- for i := 0 to Questions.Count - 1 do
- totalResult := totalResult + TStQuestionTest(Questions.Items[i]).Check;
- // Результат тестирования. Общий процент поделить на
- // кол-во вопросов
- result := Round( (totalResult / Questions.Count) );
- end;
Кнопка «Закончить»
При клике на кнопку «Закончить» показывается диалог с результатами, и в зависимости от выбора тестируемого либо тест начинается сначала, либо программа закрывается.
- procedure TMainForm.ResultButtonClick(Sender: TObject);
- var
- ResultForm: TResultForm;
- begin
- // Показываем форму
- ResultForm := TResultForm.Create(self, Check );
- if ResultForm.ShowModal = mrNo then
- close
- else
- begin // Ничинаем тест сначала
- Data.Load( Data.FileName );
- LoadData;
- Questions.Sort( RandomList );
- QIndex := 0;
- end;
- end;
На этом написание программы-теста закончено. Как и, наконец, закончено написание системы тестирования. В следующей, уже заключительной части, мы подведем итоги и рассмотрим плюсы и минусы нашей системы.
Скачать полностью исходники программы
delphi, тест

Комментарии (20)
А для чего делается эта система тестирования - на заказ или для себя? Потому что я на данный момент тоже занимаюсь написанием клиента тестирвоания для ВУЗа. И у нас главная задача - поддержка картинок и все делаем через utf-8 - дабы не было проблем с кодировками.
JohnnySuperb,
Писалась давно, для курсовой:)
А создание поддержки картинок, и прочее расширение системы думаю потом когда-нибудь описать в отдельном цикле статей.
Пытаюсь разобраться в Ваших трудах, но что-то не все получается.
Начинаю работать с исходниками, постоянно ругается на библиотеки из папки lib...Я их пытаюсь создать сама из ваших статей, т.к. в исходниках этой папки нет..В итоге ну могу ни как запустить программу как проект..Изучать Delphi только начала..
Если Вам не сложно, отправьте мне на электронную почту полный исходник программы с каталогами lib, create и test.
Заранее благодарна!
E-mail: Matveev-nv@ya.ru
Анна,
Выложил исходники: http://st-programming.ru/upload/file/test_(2).zip .
Ошибки которые возникали можете описать здесь, попробует разобраться... Просто когда писал статьи, приходилось несколько раз править код, а дельфи под рукой не было, поэтому могли вылезти ошибки :)
Nodlik
,
нужно модернизировать вашу систему тестирования за определённую плату, связь со мной 410383364 или же 413326021
Nodlik, "Ошибки которые возникали можете описать здесь, попробует разобраться..."
Пишу-пишу про ошибки, толку нет :)
Спасибо! Часть ошибок исчезла, но некоторые остались.
Меня настораживает ошибка при старте проекта: Error reading mainform.DoubleBuffered...Далее либо игнорируем либо продолжаем..
Запускаем старт либо компиляцию, ошибка:
[Error] StData.pas(64): Field definition not allowed after methods or properties
[Error] StData.pas(65): ',' or ':' expected but 'CONSTRUCTOR' found
[Error] StData.pas(66): Field definition not allowed after methods or properties
[Error] StData.pas(67): ',' or ':' expected but 'CLASS' found
[Error] StData.pas(68): 'IMPLEMENTATION' expected but ';' found
[Error] StData.pas(70): '.' expected but 'IMPLEMENTATION' found
[Error] StData.pas(17): Unsatisfied forward or external declaration: 'TStQuestion.Create'
[Hint] StData.pas(46): Private symbol 'XMLDocument' declared but never used
[Hint] StData.pas(49): Private symbol 'LoadData' declared but never used
[Error] StData.pas(51): Unsatisfied forward or external declaration: 'TStData.Load'
[Error] StData.pas(52): Unsatisfied forward or external declaration: 'TStData.LoadFromXml'
[Error] StData.pas(53): Unsatisfied forward or external declaration: 'TStData.LoadEmpty'
[Error] StData.pas(54): Unsatisfied forward or external declaration: 'TStData.Save'
[Error] StData.pas(56): Unsatisfied forward or external declaration: 'TStData.AppendQuestion'
[Error] StData.pas(57): Unsatisfied forward or external declaration: 'TStData.DeleteQuestion'
[Error] StData.pas(59): Unsatisfied forward or external declaration: 'TStData.GetInstance'
[Error] StData.pas(65): Unsatisfied forward or external declaration: 'TStData.Create'
[Fatal Error] MainUnit.pas(7): Could not compile used unit '..\lib\StData.pas'
Если поможете, я буду Вам очень благодарна!
Премного благодарю, ошибок нет, всё компилится на Delphi 2010.
Да, Действительно, на Delphi 2010 все прекрасно работает!
Кому он нужен, вот прямая ссылка: http://www.delphilab.ru/files/tools/DelphiDistiller.rar
Автору огромное спасибо за помощь, Вы мне очень помогли!
отправьте мне исходник тестов на адрес sanatzmd@mail.ru
А когда будет следующая статья?
Отправил Вам письмо на электронную почту. Посмотрите, пожалуйста. За ранее спасибо.
Программа компилируется нормально. Но если пользователь пытается начать тест заново, вылетает ошибка. Если программу закрыть-открыть, то снова все нормально.
ЗЫ: "деструктор идиот дописал" :)
Николай ,
Если добавление деструктора решает проблему с началом теста заново, то подскажите, что написать - я нуб полнейший, а надо срочно
Александр, внимательно просмотрев и повторив целиком этот пример, я нашел, что в исходниках не хватает аж двух деструкторов. Добавил их, проблему с началом теста заново это не решило.
Александр, проблема найдена... где-то уничтожается переменная questions
Изменил немного процедуру:
procedure TMainForm.LoadQuestion;
var
tmpQuestion: TStQuestion;
tmpQuestions: TObjectList;
i, max: integer;
begin
Questions := TObjectList.Create; // - добавлено создание переменной
//заново(из-за нее-то и были проблемы)
Questions.Clear; // Очищаем список вопросов
// Создаем врменные список для хранения активных
// вопросов
tmpQuestions := TObjectList.Create;
теперь все работает! Автору респект!
Есть кто живой? :)
Нет никого, умерли все...
А теперь? Тут автор вообще появляется?
Беспредел, где автор сайта?! :)
Добавить комментарий