Введение в программирование на примере VBA
Часть II. Создание макроса-приложения
Занятие 12. Реализация новой версииЦель занятия – реализовать функциональность, спроектированную нами перед этим. Это занятие будет включать несколько подциклов – так как они небольшие, не будем мелочиться и объединим несколько задач в одно занятие. Подцикл 2. Чтение данных из таблицы Excel во временное хранилищеЗадача следующего подцикла – чтение данных из постоянного хранилища – таблицы Excel в только что созданное нами временное хранилище. Подобные действия производились в процедуре RecordRead. Но помещать их в эту процедуру не стоит – она вызывается неоднократно, при каждом переходе по списку вопросов, а заполнение хранилища происходит один раз. Можно, конечно, дополнить процедуру StartTest, но подобное изменение сильно усложнит эту процедуру, делая ее неудобочитаемой. Кроме того, до сих пор каждая строка в стартовой процедуре отвечала за создание или инициализацию какого-то компонента приложения, добавлять считывающий код – значит нарушать логическую однородность. Один из принципов проектирования, который следует по возможности соблюдать – каждый компонент должен выполнять какую-то одну деятельность, не надо «смешивать» разные виды деятельности в одном компоненте. Поэтому есть смысл вынести деятельность по считыванию данных в отдельную процедуру. Проектирование последовательности действийТеперь следует определить последовательность действий по чтению данных из основного хранилища.
Подобная схема – цикл со счетчиком (синтаксис мы рассматривали раньше). Единственное замечание – как правило, счетчик делается равным 0 и затем увеличивается на 1 до достижения заданного значения. Разницы нет, это – дело выбора.
Обратите внимание на проверку условия N > intQuestions ?. Как видно на схеме, увеличение счетчика происходит после выполнения «полезных» действий, поэтому последнее выполненное действие будет использовать значение счетчика N = intQuestions. В предыдущей схеме проверялось равенство нулю – но мы раньше приняли, что нумерация массива начинается с единицы. Цикл For…Next
For <index>=1 To intQuestions Не правда ли, схема была гораздо сложнее… Здесь <index> – произвольное имя переменной. Хотя это и не указано в справке VBA, но тип индекса цикла – Integer, а, значит, цикл не может иметь более 32768 проходов (если начать с нуля до 32767). Но и размер массива-хранилища ограничен тем же размером. Да и число строк в таблице Excel, оказывается, не может быть больше этого числа – так что для наших целей хватит. Решим, какие действия мы будем выполнять в цикле. Действия каждого прохода цикла Очевидно, следует передвигаться по таблице Excel, считывая значения в ячейках правильного ответа и полученного ответа и заносить эти значения в соответствующие «ячейки» временного хранилища. Это будет делаться сходно с тем, что выполнялось в процедуре RecordRead: intRow =
((<index> – 1) * LineCount) + 1 Вначале рассчитываем значение вспомогательной переменной intRow – также и для того же, что и в процедуре RecordRead, – значения, с которыми будем работать, находятся не в каждой строке таблицы, а через каждые LineCount строк. Значение же LineCount равно числу ответов на один вопрос. Значение второй и третьей строк должно быть понятно, это подробно рассматривалось при создании процедуры RecordRead.
For i = 1 To
intQuestions Реализация – процедура чтения данныхДостаточно добавить объявление переменной intRow и можно пользоваться: Public Sub
SheetRead() Как видите, добавлена строка установки флага в False. Хотя VBA при создании самостоятельно присваивает логическим переменным значение False, эта строка – для надежности и уверенности в результате. Не следует доверять тому, что VBA делает самостоятельно. Кроме того, добавлена конструкция With – для «облегчения» кода.
Но мы условились, что выполняться эта процедура будет при запуске приложения.
Public Sub
StartTest
()
Сообщений об ошибках не должно быть. Отладка – код для проверки правильности выполненияХотелось бы проверить работу созданного только что кода.
А именно, в процедуру RecordRead: Public Sub
RecordRead(intRec As Integer) arStore(intRec).Well & vbCrLf & _ "полученный ответ № " & arStore(intRec).Answ
При запуске приложения и при переходе на следующий вопрос должно появляться окно сообщения с номером отображаемого вопроса, номером правильного и номером данного ответа. Работа со строками VBA Как видите, вызывается MsgBox с каким-то странным аргументом. В нем перемежаются строки текста, переменные и какие-то символы. Здесь происходит динамическое формирование строки, которая будет выведена функцией MsgBox. Символ & обозначает, что строки слева и справа от него «складываются», формируя одну строку, это можно пояснить схемой: <строка_1> & <строка_2> = <строка_1строка_2> VBA сам переводит значения переменных в строки для построения результирующей строки (например, переменная intRec, встречающаяся в коде – типа Integer, VBA незаметно для программиста переводит ее в тип String – Строка, чтобы был верный результат). Переменная intRec в коде – номер записи (то есть номер вопроса теста). «Ячейки» массива-хранилища определяются именно согласно номерам вопросов, что закономерно. Строка vbCrLf – служебная константа VBA, при появлении ее в данном месте будет перевод строки. Наконец, в конце первой и второй строчек можно заметить знаки подчеркивания. Это – служебные знаки для придания удобочитаемости текста при работе в IDE VBA. Длинная строка кода, такая, как здесь, может быть разбита на несколько небольших подстрок, VBA воспринимает их как одно целое. Для этого делается так: <строка_1> _ То есть в конце первой подстроки помещается пробел, потом – знак подчеркивания, а затем – строка переводится (нажатием [Enter]). Имейте в виду, что разбивать таким способом можно только в местах пробелов в коде, то есть имена переменных, объектные структуры и подобные конструкции не переносятся, при попытке такого переноса VBA сообщит об ошибке. IDE VBA позволяет переносить одну строку до 16 раз. Заключительная стадия – комментирование кода.
Подцикл 3. Занесение полученного ответа во временное хранилищеМы справились с чтением данных их таблицы Excel во временное хранилище. Задача следующего подцикла – занесение во временное хранилище ответа, полученного во время тестирования. Ввиду простоты этой задачи объединим ее с другой – а именно, будем определять вопросы, на которые давался ответ в течение настоящего сеанса работы. ПроектированиеВзаимодействие объектов Занесение ответа в хранилище сделать просто. При завершении работы с текущим вопросом (при нажатии кнопки перехода) ячейке хранилища, соответствующей данному вопросу, следует присвоить значение выбранного ответа. Следует иметь в виду, что переход может быть как вперед по набору вопросов, так и назад, видимо, будут задействованы обработчики событий Click кнопок cmdNext и cmdPrev. Псевдокод будет таким: <при переходе> Вот и все. Есть ли смысл выделять этот код в отдельную процедуру – решайте сами, но, наверное, это лишь усложнит программу.
arStore(intCurrentRecord).Answ = AnswSelected
Private Sub cmdNext_Click() Обратите внимание, что занесение значения полученного ответа в хранилище происходит ДО изменения переменной intCurrentRecord. Действительно, изменение этой переменной обозначает переход к другой позиции – не с той, с которой следует работать. Неверное определение места размещения строки – источник частых ошибок.
Теперь перейдем ко второй задаче и займемся определением того, был ли дан ответ на текущий вопрос. Шаблоны проектирования Удобно делать это, отслеживая нажатие (Click) на переключателе. Независимо от того, было изменено значение переключателя или нет, щелчок на нем обозначает ответ на вопрос. Вы можете впоследствии изменить стратегию, например, ввести специальную кнопку, нажатие на которую будет утверждать, что ответ получен. Вариантов множество, дело в соответствии требованиям – и в личных предпочтениях. Интуитивное указание момента определения ответа на вопрос в данном случае очевидно. В более сложных программах это может вызвать некоторые затруднения. Для того чтобы избежать непродуктивных раздумий, следует держать в памяти абстрактную схему- шаблон, обозначающую, как в подобной ситуации будут взаимодействовать любые объекты. Для нынешней ситуации определение шаблона будет таким: Некую обязанность выполняет тот объект, который имеет «знания» и «возможности» для выполнения. Название подобного шаблона, общепринятое у проектировщиков – Эксперт. Именно соответствие шаблону Эксперт побудило нас выбрать данный вариант решения. Введение дополнительной кнопки, предназначенной только для индикации, что вопрос отвечен, противоречит шаблону и усложняет программу, делая ее сложнее для понимания. Заметим, что все, сделанное нами ранее, также соответствовало тем или иным шаблонам проектирования. Но не впадайте в уныние из-за того, что вам пока неизвестны шаблоны. Основное положение, лежащее в основе шаблонов – интуитивная понятность, очевидность и практичность. Поэтому, следуя этим принципам, вы сможете проектировать приложения ничем не хуже, чем профессиональные программисты. К сожалению, изучение шаблонов проектирования не входит в наши задачи из-за своей объемности. Исходя из вышесказанного, очень просто составить псевдокод, предназначенный для определения отвеченного вопроса: <переключатель>_Click Иными словами, при нажатии на переключатель (любой из имеющихся в группе) в соответствующую этому вопросу ячейку хранилища заносится флаг, обозначающий, что ответ на вопрос был получен. Видна некоторая избыточность этого кода. Значение флага будет заноситься в любом случае, даже если уже занесено. Возможно сделать проверку, и в случае, если флаг уже установлен, не повторять это действие. Такая проверка будет целесообразна в случае, если выполняются какие-то сложные и объемные операции, здесь же – наипростейшее присваивание, любая проверка сама по себе усложнит программу. Оставим, как есть. Реализация
Private Sub optAnswer1_Click() Как видите, в трех обработчиках событий мы разместили полностью идентичный код. По тем же соображениям, что приводились ранее, не будем выделять этот код в отдельную процедуру, хотя в более сложных случаях это следовало бы сделать.
Подцикл 4. Дополнение кода перехода по списку вопросовВо время данного подцикла перед нами две подзадачи:
В первом случае мы будем использовать значение флага из временного хранилища. Во втором – значение полученного ответа оттуда же. Займемся первой подзадачей. Подподцикл 1. Запрещение продвижения впередЗапрещение перехода вперед по списку вопросов сделать легко. Для этого достаточно «заблокировать» кнопку cmdNext – а VBA позволяет это сделать. Очевидно, блокирование надо делать при переходе к текущему вопросу, если он еще не отвечался. Если текущий вопрос уже отвечался ранее – блокировать не надо. Когда будет дан ответ на текущий вопрос – следует «разблокировать» cmdNext. Проектирование
Вот схема блокирования кнопки cmdNext:
Методологически правильнее было бы принудительно разблокировать кнопку cmdNext в случае, когда текущий вопрос уже отвечался. Но в данном случае это можно опустить. Ведь при переходе к текущему вопросу мы использовали cmdNext, которая, следовательно, была разблокирована – иначе переход был бы невозможен, – и, значит, менять ее состояние не нужно.
Мы не предусмотрели разновидность перехода по списку вопросов – а именно, переход назад. Действительно, здесь переход будет всегда происходить на уже отвеченный вопрос и поэтому следует всегда разблокировать cmdNext.
Определимся, где мы будем размещать код, выполняющий нужные действия. Взгляните на схему перехода вперед по списку. Здесь уже есть указание на размещение его в обработчике события нажатия кнопки cmdNext. Проверка получения ответа на вопрос будет размещена в обработчиках событий нажатия на переключатели (там же, где мы устанавливали флаги). А разблокирование cmdNext при переходе назад очевидно поместить в обработчик события нажатия cmdPrev. Реализация Сложнее всего реализовать первый участок кода.
<нажатие cmdNext> За блокирование кнопок (и других элементов управления) отвечает свойство Enabled (Разрешено).
<нажатие cmdNext> Чтобы определить, «отвечался» ли текущий вопрос, следует проверить значение соответствующего флага в хранилище. Определение, отвечался ли вопрос Для этого сначала уточним свое местонахождение в наборе вопросов. Для этого была создана переменная intCurrentRecord. В коде уже есть строка: intCurrentRecord = intCurrentRecord + 1 и новый код следует поместить после нее. <нажатие cmdNext> Тогда условие в блоке If...Then приобретет вид: arStore(intCurrentRecord).Flag = True Здесь мы проверяем, установлен ли в True флаг, соответствующий текущему вопросу.
<нажатие cmdNext> Уже можно вносить коррективы в проект.
cmdPrev.Enabled =
True Весь код обработчика приводить не будем – он уже достиг довольно больших размеров. Если вы тщательно придерживались процесса, приведенного в этой книге, то без труда найдете место для кода. В обработчик нажатия кнопки cmdPrev следует внести строку «разблокирования» кнопки cmdNext.
Не забудьте, что код следует поместить уже после изменения счетчика intCurrentRecord.
intCurrentRecord =
intCurrentRecord – 1 Как видите, здесь также не приводится полный код процедуры. Вам не должно составить труда найти нужное место. И, наконец, следует изменить обработчики события нажатия на переключатели. Помещаемый код тот же, что и в предыдущем случае. Private Sub optAnswer1_Click()
Как видите, в каждом из трех обработчиков события нажатия на переключатель появилось по две совершенно одинаковых строки. Пока еще выделение этих строк в отдельную процедуру накладно, но, если появится третья строка – тогда следует создать процедуру, поместить в нее эти повторяющиеся строки и вызывать процедуру в нужном месте.
Отладка Все работает, за одним исключением. А именно, сразу после запуска теста, пока вы находитесь на первом вопросе, кнопка cmdNext разблокирована! Действительно, перехода по списку еще не было, проверка флага не проводилась, кнопка должна быть недоступна для нажатия. Блокирование кнопки при запуске приложения Проще всего сразу заблокировать кнопку cmdNext при запуске формы.
Private Sub UserForm_Initialize()
Все работает! Подподцикл 2. Восстановление ответов на переключателяхВторая задача, которую следует сейчас решить – восстановление значения полученных ответов на переключателях. При запуске теста эти значения будут взяты из постоянного хранилища – таблицы Excel, а при переходе по списку вопросов – из временного хранилища. Теперь понятно, для чего мы считывали таблицу Excel при запуске теста во временное хранилище. Благодаря этому данная задача упрощается и сводится к чтению данных только из временного хранилища. Проектирование
Как видите, эта последовательность проделывается в принципиально различных ситуациях – при запуске тестового приложения и при переходе по списку вопросов. Кроме того, вероятно, эта цепочка будет состоять из нескольких строк кода. Есть смысл оформить ее как отдельную процедуру.
<процедура> <Считывание значения> из временного хранилища происходит так: <значение> = arStore(index).Answ Помещение значения в переключатель – более сложное действие. Значение переключателя («включено-выключено») определяется свойством Value. Для переключателей (OptionButton) в VBA это свойство может принимать лишь два значения – True и False. Но в хранилище находится номер полученного ответа. Следует преобразовать номер в значение True для переключателя. К сожалению, VBA не позволяет организовывать массивы элементов управления (в отличие от своего «брата» – VB). Если бы массивы контролов были возможны, код выглядел бы так (проанализируйте его сами): optAnswer(arStore(index).Answ).Value = True Для VBA придется немного подумать и усложнить код. Конструирование процедуры проверки полученного ответа и установки переключателя Первое, что следует сделать – «сбросить» все переключатели, чтобы ни один не был включен. Далее создадим проверку If...Then...Else, аргументом конструкции будет значение полученного ответа. В зависимости от результата проверки будет установлен тот или другой переключатель.
Проверка допустимости полученного ответа Заметьте, что мы, помимо проверки номера полученного ответа, предусмотрели проверку на допустимость ответа вообще (внизу схемы). Так общепринято проверять данные. Действительно, где гарантия, что некий злоумышленник не испортил данные в таблице Excel? Если не сделать подобную проверку, тест-программа может «рухнуть» или, по меньшей мере, выдать неудобопонятные значения, а вы не будете знать, в чем дело.
<Процедура> Как видите, псевдокод очень похож на код VBA! Это значит, что схема, составленная нами, удовлетворительна. Обратите внимание на <индекс>. Очевидно, что это – порядковый номер текущей записи – intCurrentRecord. Можно брать значение этой переменной, а можно создать для процедуры параметр. Методологически правильным будет создание параметра. Решим, каким образом наша процедура будет сообщать программе об ошибке. Функция, код ошибки В таких языках, как C, ошибки обрабатываются общепринятым способом. А именно, процедура возвращает значение, говорящее об ошибке. Вспомним понятие функции. Функция – процедура, возвращающая значение. Этого определения на настоящем этапе вполне достаточно. Синтаксис функции сходен с таковым для процедуры: Function
<имя_функции>(<список_параметров>) As <тип> Первая строка почти такая же, как у процедуры. Разница в слове Function – Функция, вместо Sub, и в обязательном указании <типа> возвращаемого значения. От версии к версии VBA список допустимых <типов> может дополняться. Подробнее об этом – в справке VBA. Завершающая строка также не должна вызвать затруднений. Обратите внимание, что в коде функции должна быть минимум одна строка, в которой определяется значение, возвращаемое функцией.
<имя>() As Boolean Возврат функцией значения типа Boolean – то есть логического значения, – целесообразен тогда, когда нам нужно лишь знать, успешно выполнение или нет. Если же мы хотим узнать подробно, в чем причина неудачи, то правильнее сделать тип функции Integer. Предположение ошибки в начале функции – также общепринятый прием. Во-первых, следование схеме построения функции убережет от ошибок, во-вторых, такое предположение позволит сократить конструкцию проверки.
Function SetOptBtn(ind As Integer) As Boolean Обратите внимание на последний блок Else. Строка Exit Function принуждает VBA совершить выход из функции без выполнения дальнейшего кода. Здесь выход будет произведен в случае ошибки. А если выполнение продолжится – что может случиться только при «легальном» выполнении, – то функции вернет значение True. Определим, в каком модуле следует расположить эту функцию. В коде функции неоднократно встречаются обращения к элементам управления, расположенным на форме, поэтому следовало бы поместить функцию в модуле формы. Однако, к сожалению, VBA не позволяет делать публичными функции, размещенные в формах. Придется немного усложнить код, чтобы можно было поместить его в модуль. Проблема в том, что, когда функция находится в обычном модуле, VBA не сможет найти нужный контрол, чтобы выполнить нужные действия. Следует явно указать имя формы, содержащей контрол, даже, если форма одна, как в нашем случае. То есть все слова типа optAnswer1 следует заменить на frmTest.optAnswer1 В новой функции шесть обращений к элементам управления, размещенным на форме. Есть смысл применить конструкцию With.
Полностью функция будет выглядеть так: Реализация Public Function
SetOptBtn(ind As Integer) As Boolean
Теперь надо решить, в какой момент будет происходить вызов этой функции. Вспомним, что деятельность, запрограммированная при ее помощи, должна производиться при переходе к вопросу. При создании тест-приложения мы уже сталкивались с подобным условием. А именно, при переходе от вопроса к вопросу вызывалась процедура RecordRead. Новая функция будет вызываться вместе с процедурой RecordRead. Не рекомендуется объединять новую функцию и процедуру. Это несложно технически, но при подобном «упрощении» кода путем уменьшения числа процедур произойдет соединение разнородных, осуществляющие различные виды деятельности участков кода, что приведет к неочевидности создаваемых процедур, и, в результате – к сложностям при отладке и дальнейшей разработке. Подобный способ «упрощения» приведет лишь к значительному усложнению программы. Кратко рассмотрим способ проверки ошибок, применяемый в данном случае. Вызываемая функция возвращает код ошибки (или успеха, – мысля абстрактно, успех есть такая ошибка, при которой выполнение прошло нормально, «нулевая», «пустая» ошибка). В коде, в том месте, где происходит обращение к функции, помещается конструкция If...Then, где и проверяется значение, возвращенное функцией. В результате проверки программа «решает», какие действия будут производиться далее.
If
<функция> = <код успеха> Then В нашем случае ситуация проста – функция выполнена либо успешно, либо с ошибкой. Но, как видите из псевдокода, возможно проверять различные возвращаемые значения. Так будут предусматриваться различные действия в зависимости от типов ошибок.
Действия при ошибке
RecordRead intCurrentRecord
RecordRead
intCurrentRecord Сочетания вызова функции и проверки условия В языке VBA вполне допустимо сочетать вызов функции и проверку условия, как и сделано в этом случае. При этом сначала будет происходить вызов функции, возвращенное значение которой будет подставлено в код. Вот как делает VBA незаметно для программиста: <промежуточная переменная> = SetOptBtn(intCurrentRecord) Эта конструкция совершенно аналогична предыдущей, но немного сложнее для понимания. VBA самостоятельно делает нужные подстановки, избавляя от этого разработчика. Давайте отредактируем код. Первый раз процедура RecordRead вызывается при запуске приложения, в процедуре StartTest.
Возможное затруднение в том (особенно при объемных проектах), что вы можете не помнить, где расположены нужные участки кода. Поиск «вручную» долог и сопряжен с ошибками. Среда VBA имеет средство для облегчения задачи.
Появится диалоговое окошко поиска:
Как видите, в верхнем поле – слово, выбранное нами перед вызовом окна. Ниже слева – переключатели, определяющие область поиска – в текущей процедуре, в текущем модуле или проекте. В данный момент недоступен переключатель поиска в выделенном тексте (вы можете самостоятельно выделить участок модуля и производить поиск только в этом участке, тогда данный переключатель будет доступен). Справа – «падающий» список, где указывается направление поиска – вперед по тексту, назад (к началу) или везде. В данном случае уже выбрано направление All – Везде. Ниже – флажки для задания дополнительных условий поиска – поиск только целого слова, поиск с учетом регистра букв и поиск по шаблону. В нашем случае эти возможности не нужны. Предположительно, поиск будет производиться во всем проекте.
Вы увидите, что в окне кода появился участок текста, содержащий искомое слово. Подскажем – будет три вхождения слова RecordRead, в том числе и заголовок этой процедуры. Заголовок, естественно, изменять не надо.
VBA имеет средство и для замены текста, но оно не может заменять слово на несколько строк, поэтому замену придется проводить вручную. Как видите, произошло «размножение» строки-сообщения об ошибке. Следует исправить эту ситуацию.
Public Const strError As String = "Неверно значение полученного ответа!" После чего все строки MsgBox "Неверно значение полученного ответа!"
MsgBox strError В будущем следует сразу использовать строковые константы, не доводя дело до многократного и ненужного изменения кода.
Второй «виток» создания тестового приложения закончен.
Итог занятия.Не смотря на внушительные размеры, это занятие не дает ничего принципиально нового. По большому счету, мы лишь повторяем пройденное ранее и применяем уже имеющиеся знания к новым задачам. Обратите внимание, что мы активно использовали конструкции If...Then и For...Next, которые теоретически изучались в первой части учебника. Тогда вам давался синтаксис и объяснение – сейчас же перед нами примеры использования. В случае затруднений – перелистайте начало учебника (хотя лучше было бы вновь составить конспект). Кроме того, вы познакомились с приемами составления строк из констант и переменных (правильное название – конкатенация строк) и с некоторыми приемами работы в IDE VBA. Важная теоретическая составляющая занятия – понятие функции. Можете написать на эту тему несколько строк в свою тетрадь конспектов. «Многоэтажная» конструкция If...Then может быть заменена другой конструкцией, имеющейся в языке VBA – Select Case (Выбрать в Случае). Изучите ее самостоятельно и занесите описание в конспект. Но заметим, что «многоэтажный» If...Then совершенно аналогичен Select Case и выбор – в ваших личных предпочтениях, поэтому мы и не применяли новую конструкцию в коде. Обратите внимание на концепцию функции, возвращающей код ошибки (успешности выполнения). В подобном стиле можно написать всю программу, это будет удобно при отладке – но возникнут некоторые сложности, если вам понадобится какое-то «полезное» значение от этой функции. Поэтому запомните, что так можно делать – и оставьте подобный прием до тех пор, пока вы не станете мастером программирования VBA. |