Эта задача интересна тем, что она отчетливо демонстрирует ситуацию, когда начальный отрезок ряда чисел вычисляется по "своим" формулам и лишь, начиная с третьего числа, идут вычисления по рекуррентным соотношениям. Для того чтобы в каждый момент иметь не весь ряд вычисленных чисел, а только последние полученные значения, нам потребуются три ячейки таблицы Excel. Заметьте, двумя ячейками не обойтись. Формулы в двух первых ячейках используют конструкцию IF, позволяющую на первом шаге вычислений задать соответствующую константу, а затем перейти к рекуррентному соотношению. Третья ячейка необходима, как дополнительная память при пересылке значений. Для реализации задачи я выполнил следующие действия:
В ячейку Fprev занес IF - формулу: "=ЕСЛИ(Fprev=0; 1; Fib)"
В ячейку Fib занес IF - формулу: "=ЕСЛИ(Fib=0; 1; Fnext)"
В ячейку Fnext ввел формулу: "=Fprev + Fib"
Эти формулы порождают на первом шаге вычислений тройку чисел (1,1,2), на втором - (2,3,5) и т.д.
Напомним, что важен порядок расположения этих формул на рабочем листе. Они должны идти сверху вниз, слева направо.
Взгляните, как выглядит на рабочем листе решение задач по нахождению корня уравнения и вычисления чисел Фибоначчи, использующее циклические вычисления.
увеличить изображение Рис. 2.2. Циклические вычисления и итерационные процессы
Циклические вычисления и нахождение корней уравнения
Покажем, как можно использовать циклические вычисления на примере задачи нахождения корня уравнения методом Ньютона. Для простоты я начну с квадратного уравнения, а позже рассмотрю и более "серьезные" уравнения. Итак, рассмотрим квадратное уравнение: X2 -5X+6 =0. Найти корень этого (и любого другого уравнения) можно, используя всего одну единственную ячейку Excel. Для этого достаточно включить режим циклических вычислений и ввести в произвольную ячейку с именем, скажем X, рекуррентную формулу, задающую вычисления по Ньютону: = X - F(X)/F1(X),
где F и F1 задают соответственно выражения, вычисляющие функцию и производную. Для нашего квадратного уравнения после ввода формулы в ней появится значение 2, соответствующее одному из корней уравнения. А как получить второй корень? Обычно, это можно сделать путем изменения начального приближения. В нашем случае начальное приближение не задавалось, итерационный процесс вычислений начинался со значения, хранимого в ячейке X по умолчанию и равного нулю. Как же задать начальное приближение в циклических вычислениях? Возникшая проблема не связана с данной конкретной задачей. Она возникает всегда в циклических вычислениях, - до начала цикла надо задать начальные установки. В рекуррентных соотношениях всегда есть некоторый начальный отрезок. Решать задачу задания начальных установок в каждом случае можно по-разному. Я продемонстрирую один прием, основанный на использовании функции ЕСЛИ. Вот как выглядит "настоящее" решение этой задачи, использующее 4 ячейки, две из которых нужны по существу дела, а две используются для повышения наглядности процесса вычислений:
В ячейку с именем Xinit я ввел начальное приближение.
В ячейку Xcur, в которой и будет идти циклический счет, ввел формулу: = ЕСЛИ(Xcur =0; Xinit; Xcur - (6- Xcur *(5- Xcur))/(2* Xcur -5))
В две другие вспомогательные ячейки я поместил текст этой формулы и формулу, задающую вычисление функции в точке Xcur, позволяющую следить за качеством решения.
Заметьте, что на первом шаге вычислений, функция IF (ЕСЛИ) поместит в ячейку Xcur начальное значение, а затем уже начнет счет по формуле на последующих шагах.
Чтобы сменить начальное приближение, недостаточно изменить содержимое ячейки Xinit и запустить процесс вычислений. В этом случае вычисления будут продолжены, начиная с последнего вычисленного значения. Чтобы обнулить значение, хранящееся в ячейке Xcur, нужно заново записать туда формулу. Для этого достаточно выбрать ячейку и выделить текст формулы непосредственно в окне ее редактирования. Щелчок по Enter начнет вычисления с новым начальным приближением.
Циклические вычисления
Если зависимые ячейки Excel образуют цикл, то говорят, что имеют место циклические ссылки (circular references). В обычном режиме Excel обнаруживает цикл и выдает сообщение о возникшей ситуации, требуя устранить циклические ссылки. Следуя обычной семантике, он не может провести вычисления, так как циклические ссылки порождают бесконечные вычисления. Есть два выхода из этой ситуации, - устранить циклические ссылки или изменить настройку в машине вычислений так, чтобы такие вычисления стали возможными. В последнем случае, естественно, требуется, чтобы число повторений цикла было конечным. Excel допускает переход к новой семантике, обеспечивающей проведение циклических вычислений. Вручную, для этого достаточно на вкладке Вычисления (меню Сервис, пункт Параметры) включить флажок Итерации и при необходимости изменить число повторений цикла в окошке "Максимум итераций". Можно также задать точность вычислений в окошке "Максимальное изменение", что также приводит к ограничению числа повторений
цикла. По умолчанию максимальное число итераций и точность вычислений соответственно имеют значения 100 и 0,0001. Понятно, что включить циклические вычисления и задать значения параметров, определяющих окончание цикла, можно и программно. Укажем, особенности семантики циклических вычислений:
Формулы, связанные циклическими ссылками, вычисляются многократно.
Запись формул на листе определяет порядок их вычисления. Формулы вычисляются сверху вниз, слева направо.
Число повторений цикла определяется параметрами, заданными на вкладке Вычисления. Цикл заканчивается при достижении максимального числа итераций или, когда изменения значений во всех ячейках не превосходят заданной точности.
В каких же ситуациях требуется прибегать к циклическим вычислениям? Это, возможно, следует делать, когда речь идет о реализации итерационного процесса, вычислениях по рекуррентным соотношениям. У нас уже были примеры реализации итерационных процессов, например, вычисление суммы ряда, задающего экспоненту, в которых не применялись циклические ссылки. Платой за это было использование дополнительных ячеек таблицы Excel. Правда, появлялись и новые возможности, - возможность построить график, проанализировать процесс сходимости и т.д. Тем не менее, программисту, привыкшему к традиционным языкам, и привыкшему "с детства" экономить на переменных, может показаться странным предложенное решение задачи о нахождении корня уравнения, где на экран выводятся результаты всех приближений. В Excel экономия ячеек не главная задача. Тем не менее, при реализации итерационных процессов можно, конечно, и в Excel иметь одну единственную ячейку X, значение которой изменяется, начиная от начального прибли жения до искомого результата. Это в большей степени соответствует понятию переменной в языках программирования.
Функции с побочным эффектом и неявная передача данных
Возможность написать функцию с побочным эффектом или неявной передачей данных является одной из основных причин вычисления всех пользовательских функций при пересчете электронной таблицы. Давайте приведем примеры, проясняющие ситуацию. С этой целью я написал три функции:
ПравильнаяФункция( X As Variant) As Variant. Это пример хорошей, правильно построенной функции. Через параметр X ей передается значение некоторой ячейки рабочего листа (объект Range). В качестве результата она возвращает значение функции,- в нашем примере результат является копией входного параметра X.
ПобочныйЭффект(X As Variant, Y As Variant) As Variant. В данной функции помимо вычисления результата изменяется и значение параметра Y. Поскольку по умолчанию параметр передается по ссылке (By Ref), то это должно было бы привести к побочному эффекту и изменить содержимое ячейки рабочего листа, переданной в качестве параметра Y. Мы увидим, что этого, однако, не происходит.
НеявнаяПередача(X As Variant) As Variant В данной функции результат зависит не только от входного параметра X, но и от значения другой, неявно используемой ячейки рабочего листа.
Вот как выглядят описания наших функций: Public Function ПравильнаяФункция(X As Variant) As Variant 'При вызове функции в формуле рабочего листа ей может быть передан объект 'Range - отдельная ячейка или диапазон. Возвращаемый результат 'также является объектом Range. Если передается и возвращается массив, 'то, естественно, функция должна вызываться в формуле над массивами.
ПравильнаяФункция = X End Function Public Function ПобочныйЭффект(X As Variant, ByRef Y As Variant) As Variant 'Также, как и ПравильнаяФункция данная функция возвращает 'в качестве результата переданный ей параметр X. ПобочныйЭффект = X
'Побочным эффектом является изменение параметра Y, переданного по ссылке. 'Однако заметьте, это изменение не затрагивает ячеек рабочего листа! Y = X
'Попытка явного изменения значений ячеек рабочего листа 'также не приводит к успеху. В этом случае и функция не возвращает 'правильный результат. Ее результат в этом случае - #ЗНАЧ. 'Range("C4") = 777
Const mes1 = "Если объект Range, переданный " Const mes2 = " в качестве второго аргумента функции ПобочныйЭффект" Const mes3 = " хранит значение, несовпадающее с первым аргументом, " Const mes4 = " то побочный эффект отсутствует!" MsgBox (mes1 & vbCrLf & mes2 & vbCrLf & mes3 & vbCrLf & mes4) End Function
Public Function НеявныеДанные(X As Variant) As Variant 'Передача данных из рабочего листа в функцию, 'минуя параметры, является возможной! Dim R As Range Set R = Range("C4") MsgBox ("В ячейке C4 хранится значение " & R.Value) НеявныеДанные = X.Value + R.Value End Function
Вот как выглядит рабочий лист Excel, на котором вызываются эти функции:
Рис. 2.1. Побочный эффект и неявная передача данных
Анализируя полученные результаты, обратим внимание на следующие моменты:
Функция, которую я назвал "ПравильнаяФункция", при вызове ее из рабочей формулы получает объект Range в качестве своего параметра X, и возвращает объект Range в качестве результата выполнения функции. Объект Range может быть единственной ячейкой, и в этом случае функция может вызываться в обычной рабочей формуле. Если же функция получает массив ячеек и возвращает массив, то она должна вызываться в формуле над массивами. На рабочем листе я продемонстрировал оба способа вызова функции. В ячейку D4 я записал формулу "=ПравильнаяФункция(B4)". Поскольку функция в качестве результата возвращает переданный ей аргумент, то значение, записанное ячейку D4, будет совпадать со значением 17, хранящемся в ячейке B4. Затем я в ячейки D5:E5 записал формулу над массивами "{=ПравильнаяФункция(B4:C4)}". В результате эта же функция позволяет скопировать диапазон ячеек.
В функции ПобочныйЭффект, хотя Y и получает "правильное" значение переменной X, но это никак не сказывается на значении ячейки рабочего листа, переданной в качестве параметра Y, хотя параметр передается по ссылке. Так что можно полагать, что передача объектов Range рабочего листа Excel в функцию всегда происходит по значению, а не по ссылке.
Всякая попытка явно или неявно изменить значения ячеек рабочего листа в процессе работы функции рабочего листа помимо возвращаемого функцией результата оканчивается неуспехом. Более того, попытка явно изменить значение в ячейке приводит к тому, что результат функции становится неопределенным. Так что побочный эффект во всех его проявлениях запрещен. Изменить содержимое листа можно только, возвращая результат работы вызываемых функций в формулах рабочего листа.
В функции НеявныеДанные создается локальный объект Range. Он получает значение одной из ячеек рабочего листа, и это значение влияет на результат, возвращаемый функцией. Тем самым становится возможной неявная передача данных, минуя аппарат формальных параметров. Заметьте, что Excel не может обнаружить такой способ зависимости между ячейками. Мы специально отразили существующие, по мнению Excel зависимости. Как видите, Excel не подозревает, что ячейка D10 зависит от ячейки C4. Именно возможность неучтенных зависимостей заставляет Excel полностью проводить вычисления всех наличествующих пользовательских функций.
Ни функции с побочным эффектом, ни функции с неявной передачей данных не вызывают никаких предупреждающих сообщений.
Const mes1 = "Если объект Range, переданный " Const mes2 = " в качестве второго аргумента функции ПобочныйЭффект" Const mes3 = " хранит значение, несовпадающее с первым аргументом, " Const mes4 = " то побочный эффект отсутствует!" MsgBox (mes1 & vbCrLf & mes2 & vbCrLf & mes3 & vbCrLf & mes4) End Function
Public Function НеявныеДанные(X As Variant) As Variant 'Передача данных из рабочего листа в функцию, 'минуя параметры, является возможной! Dim R As Range Set R = Range("C4") MsgBox ("В ячейке C4 хранится значение " & R.Value) НеявныеДанные = X.Value + R.Value End Function
Вот как выглядит рабочий лист Excel, на котором вызываются эти функции:
Рис. 2.1. Побочный эффект и неявная передача данных
Анализируя полученные результаты, обратим внимание на следующие моменты:
Функция, которую я назвал "ПравильнаяФункция", при вызове ее из рабочей формулы получает объект Range в качестве своего параметра X, и возвращает объект Range в качестве результата выполнения функции. Объект Range может быть единственной ячейкой, и в этом случае функция может вызываться в обычной рабочей формуле. Если же функция получает массив ячеек и возвращает массив, то она должна вызываться в формуле над массивами. На рабочем листе я продемонстрировал оба способа вызова функции. В ячейку D4 я записал формулу "=ПравильнаяФункция(B4)". Поскольку функция в качестве результата возвращает переданный ей аргумент, то значение, записанное ячейку D4, будет совпадать со значением 17, хранящемся в ячейке B4. Затем я в ячейки D5:E5 записал формулу над массивами "{=ПравильнаяФункция(B4:C4)}". В результате эта же функция позволяет скопировать диапазон ячеек.
В функции ПобочныйЭффект, хотя Y и получает "правильное" значение переменной X, но это никак не сказывается на значении ячейки рабочего листа, переданной в качестве параметра Y, хотя параметр передается по ссылке. Так что можно полагать, что передача объектов Range рабочего листа Excel в функцию всегда происходит по значению, а не по ссылке.
Всякая попытка явно или неявно изменить значения ячеек рабочего листа в процессе работы функции рабочего листа помимо возвращаемого функцией результата оканчивается неуспехом. Более того, попытка явно изменить значение в ячейке приводит к тому, что результат функции становится неопределенным. Так что побочный эффект во всех его проявлениях запрещен. Изменить содержимое листа можно только, возвращая результат работы вызываемых функций в формулах рабочего листа.
В функции НеявныеДанные создается локальный объект Range. Он получает значение одной из ячеек рабочего листа, и это значение влияет на результат, возвращаемый функцией. Тем самым становится возможной неявная передача данных, минуя аппарат формальных параметров. Заметьте, что Excel не может обнаружить такой способ зависимости между ячейками. Мы специально отразили существующие, по мнению Excel зависимости. Как видите, Excel не подозревает, что ячейка D10 зависит от ячейки C4. Именно возможность неучтенных зависимостей заставляет Excel полностью проводить вычисления всех наличествующих пользовательских функций.
Ни функции с побочным эффектом, ни функции с неявной передачей данных не вызывают никаких предупреждающих сообщений.
Инструментальное средство Excel - "Решатель"(Solver)
Для транспонирования, умножения и обращения матриц Excel имеет стандартные функции: ТРАНСП, МУМНОЖ и МОБР. Но в инструментарии Excel, доступном пользователю, есть более мощное средство, позволяющее в общем случае решать задачи нелинейного программирования, - Решатель (Solver). Как частный случай, он позволяет, например, найти решение нелинейных уравнений, которые рассматривались в задачах 3 и 4 предыдущей главы, и для решения которых я применял метод Ньютона. Частным случаем для Решателя является и решение систем линейных уравнений.
Общая постановка задачи
Задачи, которые можно решить с помощью Решателя, в общей постановке формулируются так: Найти: x1, x2, … xn
такие, что: F(x1, x2, ….xn) -> {Max; Min; =Value}
при ограничениях: GI(x1, x2, …xn) -> {<= Value; >= Value; = Value} I = 1…N
Искомые переменные - ячейки рабочего листа Excel - называются регулируемыми (adjustable) ячейками. Целевая функция F(x1, x2, …xn), называемая иногда просто целью, должна задаваться в виде формулы в ячейке рабочего листа. Эта формула может содержать функции, определенные пользователем, и должна зависеть от регулируемых ячеек. В момент постановки задачи определяется, что делать с целевой функцией. Возможен выбор одного из вариантов:
найти максимум целевой функции F(x1, x2, …xn);
найти минимум целевой функции F(x1, x2, …xn);
добиться того, чтобы целевая функция F(x1, x2, …xn) имела фиксированное значение: F(x1, x2, ….xn) = A.
Функции GI(x1, x2, …xn) называются ограничениями. Их можно задать как в виде равенств, так и неравенств. На регулируемые ячейки можно наложить дополнительные ограничения: положительности и/или целочисленности, тогда искомое решение ищется в области положительных и/или целых чисел. Под эту постановку подпадает самый широкий круг задач оптимизации, в том числе решение различных уравнений и систем уравнений, задачи линейного и нелинейного программирования. Такие задачи обычно проще сформулировать, чем решить. И иногда для решения конкретной оптимизационной задачи требуются специально для нее сконструированные методы. Решатель имеет в своем арсенале мощные универсальные методы решения подобных задач: метод обобщенного градиента, симплекс-метод, метод ветвей и границ. Так что в более или менее простых случаях можно надеяться на успех. Кстати, помимо решения, делается и дополнительный анализ. Например, для задач линейного программирования делается анализ на чувствительность, позволяющий понять, насколько полученное решение нечувствительно к изменению ограничений. Предусмотрена и возможность управления процессом поиска решения.
Параметры, управляющие работой Решателя
Рассмотрим возможности управления работой Решателя, задаваемые в окне Параметры (Options):
Максимальное время (MaxTime) - ограничивает время, отведенное на процесс поиска решения. По умолчанию задано 100 секунд, что обычно достаточно для задач небольшой размерности, имеющих около 10 ограничений. Для задач большой размерности придется это значение увеличивать.
Предельное число итераций (Iterations) - еще один способ ограничения времени поиска путем задания максимального числа итераций. По умолчанию задано 100, но это число можно увеличивать до 32767. Чаще всего, если решение не получено за 100 итераций, надежд получить его при увеличении этого значения мало. Лучше попытаться изменить начальное приближение и запустить процесс поиска заново.
Относительная погрешность (Precision) - задает точность выполнения ограничений. Иногда проще изменить ограничение, отодвинув границу, чем пытаться выполнить ограничения с высокой точностью.
Сходимость (Convergence) - задается десятичной дробью, меньшей единицы, позволяя остановить процесс поиска при сходимости решения к неподвижной точке, когда относительные изменения в течение последних 5 итераций не превышают заданную дробь.
Линейная модель (Assume Linear Model) - этот флажок следует включать, когда целевая функция и ограничения - линейные функции. Эта дополнительная информация позволяет Решателю упростить процесс поиска решения.
Неотрицательные значения (Assume Non-Negative) - этим флажком можно задать ограничения на переменные, что позволит искать решения в положительной области значений, не задавая специальных ограничений на их нижнюю границу.
Показывать результаты итераций (Show Iteration Results) - флажок, позволяющий включить пошаговый процесс поиска, показывая на экране результаты каждой итерации. В сложных ситуациях, когда Решатель не находит решения автоматически, рекомендуется включать этот флажок, так как иногда можно найти точку, от которой процесс поиска уклонился в сторону.
Автоматическое масштабирование (Use Automating Scaling) - флажок автоматического изменения масштаба следует включать, когда масштаб значений входных переменных и целевой функции и ограничений отличается, возможно, на порядки. Например, переменные задаются в штуках, а целевая функция, задающая суммарную стоимость, измеряется в миллионах рублей.
Относительная погрешность (Tolerance) - задается в процентах. Указанное значение имеет смысл только для задач с целочисленными ограничениями. Решатель в таких задачах вначале находит оптимальное не целочисленное решение, а потом пытается найти ближайшую целочисленную точку, решение в которой отличалось бы от оптимального не более чем на указанное данным параметром количество процентов. Если такая точка найдена, Решатель сообщает об успехе. При большом допуске (по умолчанию 5%) может быть потеряно лучшее целочисленное решение, правда, отличающееся от найденного Решателем в пределах допуска. Для целочисленных задач допуск имеет смысл уменьшить, что я и сделал при решении транспортной задачи. Хочу еще раз обратить внимание на эту особенность решения задач целочисленного программирования. Если значение параметра Tolerance задать большим, то Решатель может остановиться раньше времени, не найдя лучшего целочисленного решения. Если же его взять малым, то наилучшее целочисленное решение будет отличаться от оптимального нецелочисленного решения на величину большую, чем ту, которая задается параметром Tolerance. В этом случае формально решение заканчивается неуспехом, поскольку найденное решение не удовлетворяет всем требованиям. Конечно, параметр Tolerance играет служебную роль, и "умный" Решатель, найдя наилучшее целочисленное решение, должен был бы уведомлять, что решение найдено, но ограничение по Tolerance не выполнено. Этого, однако, не происходит. Мы еще столкнемся с этой ситуацией при рассмотрении следующей задачи.
Сохранить модель (Save Model) - командная кнопка; позволяет открыть диалоговое окно, где можно указать имя сохраняемой модели. Имеет смысл использовать эту возможность, когда на рабочем листе несколько моделей, так как единственная модель запоминается автоматически.
Загрузить модель (Load Model) - позволяет загрузить одну из сохраненных моделей.
Есть еще несколько более специальных параметров, которыми можно управлять, варьируя процедурами, применяемыми в процессе поиска. К ним следует прибегать в тяжелых ситуациях, когда решение найти не удается.
Пользовательские функции и массивы рабочего листа
Учитывая важность проблемы передачи массивов рабочего листа в пользовательскую функцию, кратко сформулируем основные выводы еще раз:
Пользовательская функция, вызываемая в формуле рабочего листа, может принимать один или несколько массивов в качестве своих входных параметров. Эти параметры должны иметь тип Variant.
На выходе функция может сформировать только один массив, который можно передать в рабочий лист. Этот массив не может быть значением одного из параметров функции - только результатом этой функции, т. е. побочный эффект не допускается. Всякая попытка вызвать функцию с побочным эффектом обнаруживается динамически (но не в период трансляции) и приводит к ошибке при вычислении результата.
Пользовательская функция, возвращающая массив рабочего листа, должна иметь тип Variant.
Ряд важных вопросов требует специального рассмотрения. Вот лишь некоторые:
Что собой представляют формулы, вызывающие пользовательские функции, когда те получают массивы рабочего листа как параметры, но их результатом является скаляр? Вызываются ли они как формулы с массивами или как обычные формулы?
Я уже сказал, что в пользовательскую функцию можно передать массив рабочего листа, для чего необходимо, чтобы соответствующий формальный параметр имел тип Variant. Но Variant является универсальным типом, а посему можно передавать не только отдельные ячейки и массивы. Что можно передавать в качестве фактического параметра, как при этом построить обработку? Ответы на эти вопросы я дам при рассмотрении следующей задачи.
Пользовательские функции, принимающие сложный объект Range
Известно, что объект Range может представлять несмежную область и являться объединением нескольких интервалов ячеек. Иначе говоря, один объект Range может задавать несколько массивов рабочего листа. Можно ли такой объект передать пользовательской функции и, если да, то как его обрабатывать? Ответ: "можно", хотя соответствующий формальный параметр следует описывать особым образом. Процедуры и функции VBA допускают произвольное число параметров, это достигается за счет того, что один, последний по счету формальный параметр может иметь спецификатор ParamArray. В этом случае данный параметр задает фактически массив параметров с произвольным числом элементов. Именно эта техника и применяется для передачи в пользовательскую функцию сложного объекта Range, представляющего не один, а произвольное число массивов. У такой функции последний параметр должен иметь спецификатор ParamArray и быть массивом типа Variant. Рассмотрим предыдущую задачу, немного усложнив ее, полагая, что при проверке кандидата на "медианность" используется произвольное число массивов. Вот как выглядит функция, решающая эту задачу: Public Function IsMedianaForAll(Cand As Variant, _ ParamArray M() As Variant) As Integer 'Эта функция осуществляет те же вычисления, что и функция IsMediana 'Важное отличие состоит в том, что аргумент M может быть 'задан сложным объектом Range 'или представлять объединение массивов. Dim Pos As Integer, Neg As Integer Pos = 0: Neg = 0 Dim Elem As Variant For Each Elem In M 'Анализ типа параметра Elem If TypeName(Elem) = "Range" Then For i = 1 To Elem.Rows.Count For j = 1 To Elem.Columns.Count If Elem.Cells(i, j) > Cand Then Pos = Pos + 1 ElseIf Elem.Cells(i, j) < Cand Then Neg = Neg + 1 End If Next j Next i
ElseIf TypeName(Elem) = "Variant()" Then 'TypeName is "Variant()" 'Это массив, но для него, к сожалению, не всегда корректно работают, 'например, функции границ: LBound, UBound. Dim Val As Variant For Each Val In Elem If Val > Cand Then Pos = Pos + 1 ElseIf Val < Cand Then Neg = Neg + 1 End If Next Val Else MsgBox ("При вызове IsMedianaForAll один из аргументов" & _ vbCrLf & "не является массивом или объектом Range!") End If Next Elem IsMedianaForAll = Pos - Neg
End Function
Комментируя работу этой функции, отмечу:
Формально эта функция по- прежнему имеет два параметра Cand и M. Правда, теперь они поменялись местами, и параметр M стал последним. Фактически у этой функции теперь произвольное число параметров, поскольку параметр M, сохранив тип Variant, стал массивом параметров. Спецификатор ParamArray подчеркивает, что это специальный массив с произвольным числом элементов.
Для работы с массивом M используется цикл типа For Each. В цикле выделяется очередной элемент Elem типа Variant, а дальше используется уже знакомый по функции IsMediana алгоритм проверки элемента Cand.
Разбор случаев делается независимо для каждого из элементов массива M. Это значит, что на входе могут быть заданы одновременно как массивы рабочего листа, так и массивы констант Visual Basic.
На том же рабочем листе, где проводились эксперименты с формулами, вызывающими функцию IsMediana, я записал еще несколько формул, вызывающих функцию IsMedianaForAll. В этих формулах аргумент может иметь сложный вид, допуская, по существу, сколь угодно большое число параметров.
увеличить изображение Рис. 2.6. Вызов функции IsMedianaForAll, допускающей сложные объекты Range
Проанализируем четыре сделанных вызова:
=IsMedianaForAll(7;M;N). В этом вызове наш кандидат - число 7 - проверяется по отношению к объединению двух массивов рабочего листа, заданных своими именами M и N. Формальных параметров у функции два, а фактических при вызове задается три. Два последних можно рассматривать как сложный объект Range, представляющий несмежную область ячеек, задающую объединение вектора M и матрицы N. С программистской точки зрения, можно полагать, что передается массив с произвольным числом элементов, где каждый из них в свою очередь является массивом. Такой фактический параметр является допустимым значением формального параметра нашей функции, имеющего спецификатор ParamArray.
=IsMedianaForAll(4,5;N;M). В этом вызове мало нового в сравнении с предыдущим. Изменен порядок следования массивов N и M, изменен кандидат, - им стало число 4.5, не входящее ни в один из массивов. Как показывает результат, это число является медианой объединенных массивов.
=IsMedianaForAll(7; {4;7;2}; {9;12;5}). Здесь в роли аргументов выступают массивы Visual Basic, заданные в виде констант, заключенных в фигурные скобки. Фактическое значение параметра M в этом случае представляет массив из двух элементов, каждый из которых в свою очередь является массивом.
=IsMedianaForAll(7; {4;7;2}; {9;12;5}; M). Ситуация в этом вызове сложнее, так как число аргументов возросло, но, что более важно, среди них есть как массивы Visual Basic, так и массив рабочего листа - вектор M. Тем не менее, все работает правильно.
"Пользовательские" и "обычные" функции VBA
Под пользовательской функцией VBA я понимаю функцию, которая может быть вызвана в формулах рабочего листа Excel. Обычные функции VBA могут вызываться в функциях и процедурах VBA. Возникает естественный вопрос, может ли одна и та же функция одновременно быть пользовательской и обычной? Этот же вопрос может быть сформулирован и по-другому, есть ли особая специфика в пользовательских функциях? Ответ прост - особой специфики нет, и одна и та же функция может вызываться как в формулах, так и в процедурах VBA. Практически не возникает проблем, когда аргументами функции и результатом являются скалярные значения. Когда же, как в случае с MultMatr, аргументами и результатом являются массивы, то возникают определенные трудности. Эти трудности преодолимы, примером тому служит функция MultMatr. Попробуем разобраться, в чем состоят эти трудности. Когда функции нужно предать массив, то в пользовательских функциях при вызове им передаются объекты Range, обычным функциям - пер еменные, описанные, как массивы VBA. Поэтому для обеспечения универсального характера функции в ее теле необходимо производить разбор случаев, определяя тип параметра. В результате растет объем функции, а, следовательно, усложняется ее понимание. Еще одна сложность связана с результатом вычислений. Никаких проблем нет для формулы над массивами, вызывающей пользовательскую функцию, - результат, записывается в область, выделенную при вызове формулы. Обычные функции VBA, как правило, не возвращают массив в качестве результата. Если результатом работы является массив, то при программировании на VBA создается процедура, а не функция. Дело в том, что в VBA присваивания над массивами запрещены, потому просто невозможно присвоить массиву значение обычной функции, возвращающей массив в качестве своего результата. Как же, спросите Вы, MultMatr может использоваться в качестве обычной функции? Только за счет маленьких хитростей и универсального типа Variant, который может быть чем угодно, в том числе и массивом. При вызове MultMatr как обычной функции в процедуре VBA результат вызова присваивается переменной типа Variant, - это допустимо. Затем уже с этой переменной можно работать как с массивом, - это тоже допустимо, что я и продемонстрирую чуть позже. Таким образом, всегда можно написать функцию так, чтобы она служила и как пользовательская и как обычная функция. Другой вопрос, стоит ли это делать. В таком обобщении есть свой резон, поскольку в таких случаях при вызове пользовательской функции ей можно передавать в качестве аргументов не только объекты Range, но и массивы констант, что было продемонстрировано при рассмотрении функции IsMedianaForAll. Заметьте, однако, что в функцию MultMatr передать массивы констант невозможно. Причина этого в том, что для двумерных массивов констант функции UBound и LBound работают некорректно. Подводя итог, замечу, что, когда приходится работать с массивами, разумнее иметь два варианта - пользовательскую и обычную функцию. Чтобы отчетливее продемонстрировать разницу между обычными и пользовательскими функциями, я написал обычную процедуру MultMatr1, выполняющую умножение матриц. Вот ее текст: Public Sub MultMatr1(A() As Variant, B() As Variant, C() As Variant) 'Умножение матриц. 'Процедуру можно вызывать в обычных VBA функциях и процедурах, 'передавая ей в качестве параметров массивы VBA. Dim i As Integer, j As Integer, k As Integer Dim N As Integer, M As Integer, Q As Integer Dim P As Integer, NC As Integer, PC As Integer Dim msg1 As String, msg2 As String Dim Uncor1 As Boolean, Uncor2 As Boolean Dim Elem As Variant Uncor1 = True: Uncor2 = True msg1 = " При вызове MultMatr некорректно задана размерность" _ & " перемножаемых матриц!" & vbCrLf & _ "Число столбцов матрицы A = " & M & vbCrLf & _ "Число строк матрицы B = " & Q msg2 = " При вызове MultMatr некорректно задана размерность" _ & " матрицы результата!" & vbCrLf & _ "Число строк матрицы C = " & NC & vbCrLf & _ "Число столбцов матрицы C = " & PC
'Проверка корректности задания размерности N = UBound(A, 1): M = UBound(A, 2) Q = UBound(B, 1): P = UBound(B, 2) NC = UBound(C, 1): PC = UBound(C, 2) If (Q = M) Then ' Размерность исходных матриц задана корректно Uncor1 = False If NC = N And PC = P Then 'Размерность результата задана корректно Uncor2 = False 'Построение произведения матриц AB =A*B For i = 1 To N For j = 1 To P Elem = 0 For k = 1 To M Elem = Elem + A(i, k) * B(k, j) Next k C(i, j) = Elem Next j Next i Else 'некорректно задана размерность If Uncor1 Then MsgBox (msg1) If Uncor2 Then MsgBox (msg2) End If End If End Sub
От функции MultMatr она отличается тем, что в ней опущен разбор случаев и проводится более тщательная проверка корректности размерностей аргументов. Конечно, она ни в коем случае не может быть использована как пользовательская функция, но зато работать с ней в процедурах и функциях VBA с ней не то чтобы проще, но естественнее. Чтобы почувствовать разницу, я продемонстрирую тестовую процедуру, в которой вызываются, как функция MultMatr так и процедура MultMatr1.
Public Sub MultTest() Dim A(1 To 2, 1 To 2) As Variant Dim B(1 To 2, 1 To 2) As Variant Dim C(1 To 2, 1 To 2) As Variant Dim C1 As Variant Dim item As Variant Dim i As Integer, j As Integer A(1, 1) = 1: A(1, 2) = 2: A(2, 1) = 3: A(2, 2) = 4 B(1, 1) = 1: B(1, 2) = 2: B(2, 1) = 3: B(2, 2) = 4 'Переменной типа Variant присваивается массив C1 = MultMatr(A, B) For i = 1 To UBound(C1, 1) For j = 1 To UBound(C1, 2) Debug.Print C1(i, j) Next j Next i 'Здесь С - массив и работаем с ним, как с массивом. Call MultMatr1(A, B, C) For i = 1 To UBound(C, 1) For j = 1 To UBound(C1, 2) Debug.Print C(i, j) Next j Next i 'Вызов тестовой функции, возвращающей массив. C1 = ResArray(A) For Each item In C1 Debug.Print item Next item End Sub
Public Function ResArray(A() As Variant) As Variant 'Возвращает в качестве результата, 'переданный ей массив ResArray = A End Function
Как видите, функция MultMatr, успешно работающая в роли пользовательской функции, с тем же успехом может выполнять и роль обычной функции. Так что я выполнил поставленную задачу, создав "универсальную" функцию. Но, возможно, предпочтительнее в процедурах VBA работать с MultMatr1, не прибегая к переменным типа Variant. Обратите внимание на небольшую тестовую функцию ResArray, которую я написал, чтобы в явной форме продемонстрировать способ возвращения массива в функциях VBA.
Решатель и программирование
Правильная постановка оптимизационной задачи, грамотное использование Решателя требует от пользователя определенной математической культуры. В ряде случаев эту часть работы следует взять на себя программисту и организовать программный вызов Решателя, оставив за пользователем задание исходных данных. Все, что можно сделать вручную, можно сделать и на VBA и даже несколько больше, используя стандартный набор функций VBA для работы с Решателем. Кратко охарактеризую четыре основные функции:
SolverOk(SetCell, MaxMinVal, ValueOf, ByChange) - позволяет поставить задачу оптимизации. Она выполняет работу, эквивалентную установке параметров в диалоговом окне при вызове Решателя. Параметр SetCell задает ячейку, содержащую формулу с целевой функцией. Три возможных значения параметра MaxMinVal указывают, что с ней делать (максимизировать, минимизировать или пытаться зафиксировать ее значение, которое при этом задается параметром ValueOf). Последний по счету параметр ByChange задает диапазон регулируемых ячеек - переменных оптимизационной задачи.
SolverAdd(CellRef, Relation, FormulaText) - позволяет добавлять ограничения в модель. Первый параметр CellRef задает левую часть ограничения в виде ссылки на ячейку или их диапазон. Пять возможных значений параметра Relation (1…5) определяют отношение, связывающее левую и правую часть ограничения (<=, =, >=, int, bin). Два последних отношения накладывают на переменные ограничения целочисленности и двоичности. Двоичные переменные могут принимать только два значения 0 и 1. Параметр FormulaText задает правую часть ограничений, он не должен задаваться, если отношение задано как int или bin.
SolverOptions(MaxTime, Iterations, Precision, AssumeLinear, StepThru, Estimates, Derivates, Search, IntTolerance, Scaling, Convergence, AssumeNonNeg) - используя эту функцию, можно установить параметры Решателя, которые вручную устанавливаются в окне Options. Мы не будем их описывать, так как большинство из них мы фактически рассмотрели. Но на одном из них остановимся. Булев параметр StepThru имеет значение True, если включается пошаговое исполнение с прерыванием после выполнения каждой итерации. При программировании можно передать Решателю макрос, который будет выполняться при каждом таком прерывании.
SolverSolve(UserFinish, ShowRef) - запускает Решатель. Ее действие эквивалентно выбору кнопки Solve. Возможный булев параметр UserFinish равен True, если результаты не выводятся на дисплей. Если параметр опущен или равен False, результаты появятся в диалоговом окне Решателя, предназначенном для их вывода. Параметр ShowRef представляет строку, задающую имя макроса, который должен выполняться в паузах между итерациями. Он задается, только если параметр StepThru равен True.
Решение систем линейных уравнений, умножение и обращение матриц
Задачи, перечисленные в заголовке, возникают достаточно часто в различных сферах деятельности, требующих применения математического аппарата. По этой причине в библиотеке Excel есть встроенные функции, позволяющие решить эти задачи. О встроенных функциях умножения матриц МУМНОЖ (MMULT) и транспонирования матриц МТРАНСП (MTRANSP) я уже упоминал, есть и функция для нахождения обратной матрицы - МОБРАТ (MINVERSE). Зная обратную матрицу и умея умножать матрицы, найти решение системы уравнений не представляет труда. Но поскольку умение решать эти задачи входит в круг начального образования программиста, то я полагаю уместным рассмотреть создание собственных аналогов этих функций на VBA. Заодно это позволит рассмотреть некоторые важные моменты в создании пользовательских функций, вызываемых в формулах рабочего листа. Многое мы уже знаем. Знаем, как написать пользовательскую функцию, какие ограничения накладываются на ее параметры с тем, чтобы ее можно было вызывать из формул рабочего листа Excel, передавая ей в качестве фактических параметров массивы рабочего листа. Знаем, как анализировать тип переданных данных. Знаем, как такая функция может вернуть массив и изменить содержимое рабочего листа. В последующих примерах я еще раз коснусь всех этих вопросов, а, кроме того, появятся и другие вопросы, на которые стоит обратить внимание.
Решение системы линейных уравнений методом простой итерации
Циклические вычисления можно проводить и над массивами. В качестве примера такой задачи рассмотрим итеративный способ решения системы линейных уравнений AX = B. Если применить метод простой итерации, то вектор решений X определяется следующим рекуррентным соотношением: XK+1 = XK -(AXK -B) k= 1…N
Чтобы запустить процесс вычислений, нужно задать начальное приближение - вектор X1. Для сходимости процесса необходимо, но не достаточно, чтобы норма матрицы А была меньше 1. Иногда достаточно соответствующим образом масштабировать матрицу А и вектор В. Но нас конечно интересуют другие вопросы,- как реализовать на Excel это итеративное соотношение над векторами и матрицами, не прибегая к программированию на VBA и используя циклические вычисления. Покажем, что решение получить ненамного сложнее, в сравнении с применением схемы Ньютона для одного уравнения. Я сделал следующее:
Ввел матрицу А и вектор В, предварительно нормировав их. Заметим, что нужно правильно выбирать ориентацию векторов. В данном случае мне было удобнее представлять вектор В строкой. Вектор получил имя Veb.
Затем определил вектор с именем Vxinit, задающий начальное приближение.
Затем определил еще один вектор с именем Vxcur. Это вектор решений, его значения будут изменяться в цикле, пока не закончится итерационный процесс. Формула для вычислений будет определяться написанным выше рекуррентным соотношением, и представлять собой формулу над массивами. Учитывая то, что говорилось ранее о рекуррентных соотношениях, формула должна быть IF - функцией. Это позволит использовать на первом шаге начальное приближение, а потом уже вести вычисления по рекуррентной формуле. Приведем теперь саму формулу над массивами, вычисляющую рекуррентно вектор Vxcur:
{=ЕСЛИ(Vxcur=0; Vxinit; Vxcur - Axhor + Veb)}
Здесь, как и ранее, используются нулевые значения, как признак начала процесса. Заметим, что, конечно, было бы лучше, если бы существовала возможность явной инициализации переменных при циклических вычислениях, а так наши действия немного напоминают фокус. Вектор Axhor, введенный в формулу - это вспомогательный вектор, равный произведению матрицы А на текущий вектор Vxcur. Если процесс вычислений сходится, и Vxcur сходится к решению системы уравнений, то вектор Axhor будет сходиться к вектору Veb.
Опишем подробнее формирование вспомогательного вектора Axhor. Содержательно, он представляет произведение матрицы на вектор. Но произведение матрицы на вектор дает вектор столбец, а в рекуррентном соотношении необходим вектор строка. По этой причине я формирую вначале вектор столбец Axver:
Axver = A*Vxcur
Конечно, для умножения матрицы на вектор можно воспользоваться стандартной функцией, но я хочу показать, как это делается с использованием более простых средств. Для этого достаточно написать формулу, вычисляющую его первый элемент:
{=SUM(A38:B38*Vxcur)}
и затем скопировать ее по столбцу. Обратите внимание, вектор, на который умножается матрица, должен быть строкой, а вектор - результат - столбцом. Строка матрицы задается в относительных адресах и при копировании меняется. Вектор, на который умножаются строки, задается своим именем, а значит абсолютным адресом, не изменяющимся при копировании. Каждая формула, задающая элемент вектора, является формулой над массивами.
Получив вектор столбец Axver, задающий нужное произведение, можно перейти к получению строки - Axhor, представляющей результат транспонирования вектора Axver. Для транспонирования я использовал стандартную функцию Transpose. Сама задача транспонирования и эта функция подробно будет рассмотрена чуть позже. Формула над массивами, определяющая вектор Axhor имеет вид:
{=ТРАНСП(Axver)}
Задав все вектора и все формулы, я получил решение системы линейных уравнений. Как ни странно, но даже столь плохой метод, как метод простой итерации сошелся к решению. Так, начав с начального приближения (1,1), я получил решение (1.6, 2.4) с заданной точностью.
Заметим, предложенная схема носит общий характер и позволяет решать любую систему линейных уравнений, не ограничиваясь системой из двух уравнений, рассмотренную в примере. Однако никому не рекомендую применять метод простой итерации для нахождения решения системы уравнений, - для этого есть другие точные методы. Просто мне было важно продемонстрировать возможность циклических вычислений при действиях с матрицами, и нужен был достаточно простой пример.
В заключение темы о циклических вычислениях покажем, как выглядят построенное решение на рабочем листе Excel:
увеличить изображение Рис. 2.3. Циклические вычисления и действия над матрицами
Опишем подробнее формирование вспомогательного вектора Axhor. Содержательно, он представляет произведение матрицы на вектор. Но произведение матрицы на вектор дает вектор столбец, а в рекуррентном соотношении необходим вектор строка. По этой причине я формирую вначале вектор столбец Axver:
Axver = A*Vxcur
Конечно, для умножения матрицы на вектор можно воспользоваться стандартной функцией, но я хочу показать, как это делается с использованием более простых средств. Для этого достаточно написать формулу, вычисляющую его первый элемент:
{=SUM(A38:B38*Vxcur)}
и затем скопировать ее по столбцу. Обратите внимание, вектор, на который умножается матрица, должен быть строкой, а вектор - результат - столбцом. Строка матрицы задается в относительных адресах и при копировании меняется. Вектор, на который умножаются строки, задается своим именем, а значит абсолютным адресом, не изменяющимся при копировании. Каждая формула, задающая элемент вектора, является формулой над массивами.
Получив вектор столбец Axver, задающий нужное произведение, можно перейти к получению строки - Axhor, представляющей результат транспонирования вектора Axver. Для транспонирования я использовал стандартную функцию Transpose. Сама задача транспонирования и эта функция подробно будет рассмотрена чуть позже. Формула над массивами, определяющая вектор Axhor имеет вид:
{=ТРАНСП(Axver)}
Задав все вектора и все формулы, я получил решение системы линейных уравнений. Как ни странно, но даже столь плохой метод, как метод простой итерации сошелся к решению. Так, начав с начального приближения (1,1), я получил решение (1.6, 2.4) с заданной точностью.
Заметим, предложенная схема носит общий характер и позволяет решать любую систему линейных уравнений, не ограничиваясь системой из двух уравнений, рассмотренную в примере. Однако никому не рекомендую применять метод простой итерации для нахождения решения системы уравнений, - для этого есть другие точные методы. Просто мне было важно продемонстрировать возможность циклических вычислений при действиях с матрицами, и нужен был достаточно простой пример.
В заключение темы о циклических вычислениях покажем, как выглядят построенное решение на рабочем листе Excel:
увеличить изображение Рис. 2.3. Циклические вычисления и действия над матрицами
Транспортная задача
В офисной деятельности очень часто приходится анализировать варианты, стараясь найти оптимальное решение. К сожалению, математические методы оптимизации в реальной офисной практике применяются сравнительно редко. Причины этого я обсуждать здесь не буду. Наша цель другая, - показать, как решаются такие задачи. Поэтому я подробно останавливаюсь на возможностях Решателя, полагая, что это средство Excel позволяет легко и просто решать самые разные оптимизационные задачи, возникающие в ходе офисной деятельности. Рассмотрим более серьезную оптимизационную задачу - транспортную. Вот одна из возможных содержательных постановок этой задачи: Имеется N складов, на каждом из которых хранится готовая продукция. Пусть: P1, P2, …PN
- объемы продукции, хранящейся на каждом складе. Пусть теперь есть M магазинов, от которых поступила заявка на завоз этой продукции: Q1, Q2, …QM
- требуемые объемы продукции для каждого магазина. Транспортные расходы на перевозку единицы продукции из склада I в магазин J задаются матрицей: || TI,J || I = 1…N; J = 1…M
Требуется найти оптимальный план перевозок,- матрицу ||XI,J||, минимизирующую суммарные транспортные расходы при естественных ограничениях: все заявки магазинов должны быть удовлетворены, и со склада нельзя увезти продукции сверх того, что там имеется. В формальной постановке эта задача имеет вид:
XI,J* TI,J -> Min I J
При ограничениях:
XI,J <= PI I = 1…N; J
XI,J = QJ J =1…M; I
XI,J > = 0; I = 1…N; J = 1…M;
В качестве примера я рассмотрел транспортную задачу для 2 складов и 5 магазинов.
В ячейки C4:C5 записал объемы продукции, имеющиеся на 2 складах.
В ячейки E5:I5 - заявки на продукцию, поступившие от магазинов.
В ячейки B8:F9 - матрицу транспортных расходов, задающую расходы на перевозку из I-го склада в J-й магазин единицы продукции.
В ячейки B13:F14 - план перевозок - матрицу, задающую количество товара, перевезенного из I-го склада в J-й магазин. Начальное распределение плана задано по принципу "каждой сестре по серьге", равномерно распределив всю имеющуюся на складе продукцию по магазинам. Эти ячейки являются регулируемыми и Решатель должен найти более подходящее решение, изменив значения в этих ячейках.
В ячейку D15 - записал целевую функцию: {=СУММ(B8:F9*B13:F14)}
В ячейки D17:H17 записал ограничения, задающие требование о точном выполнении заявки каждого магазина. Как обычно, я записал соответствующую формулу в первую из этих ячеек: {=СУММ(B13:B14) - E5}
Затем скопировал ее. При копировании формула автоматически меняется, задавая нужное ограничение. Правда, нужно следить при этом за правильной ориентацией данных. Например, в данном случае формулу нужно копировать в строку, а не в столбец.
Затем задал следующую группу ограничений. Эти ограничения отвечают тому естественному условию, что со склада нельзя увести больше продукции, чем там имеется. Формула, помещенная в ячейку D18, имеет вид: {= С4 - СУММ(B13:F13)}
Эта формула скопирована уже по столбцу в ячейку D19. Подготовительный этап завершен - можно вызывать Решатель.
При вызове Решателя и задании параметров в его диалоговом окне выполнялась стандартная работа по указанию ячейки с целевой функцией, диапазоном регулируемых ячеек и заданием ограничений. Заметьте, помимо двух групп ограничений я задал и ограничения целочисленности переменных. Предполагается, что продукция может перевозиться только целыми единицами - бочками, мешками, ящиками. Такие ограничения в Решателе создаются совсем просто, - достаточно среди операторов, связывающих левую и правую части ограничения, выбрать оператор int. Взгляните, как выглядят результаты моей работы:
увеличить изображение Рис. 2.21. Окно Решателя при решении транспортной задачи
Прежде чем дать команду на решение задачи, я провел настройку параметров в окне Options. В частности я включил флажки, указывающие на линейность модели и положительность переменных. Кроме того, я увеличил точность решения целочисленной задачи, задав в окне Tolerance значение в 1% вместо 5%, принятых по умолчанию.
Рис. 2.22. Настройка в окне параметров Решателя при решении транспортной задачи
Осталось щелкнуть кнопку "Solve" и получить оптимальный план перевозок. Вы можете проанализировать, насколько оптимальный план отличается от равномерного распределения, предложенного в качестве первоначального варианта, и как уменьшились транспортные расходы:
увеличить изображение Рис. 2.23. Решение транспортной задачи
Задача 10 Нахождение медианы
Постановка задачи: Для массива M и элемента Cand вычислить разность между двумя числами - числом элементов массива M, больших и меньших Cand. Это вариация задачи о медиане - "среднем" элементе - массива. Медиану можно определить, например, таким алгоритмом: упорядочив массив, взять элемент, находящийся в середине. Есть и более эффективные алгоритмы. Но я решил ограничиться более простой задачей - проверкой на "медианность". Заметим: если все элементы массива M различны и число их нечетно, то, взяв медиану в качестве Cand, искомая в задаче 10 разность даст значение 0. В общем случае, близость искомой разности к нулю является мерой близости параметра Cand к медиане массива M. Но займемся программистскими аспектами этой задачи. У функции, ее реализующей, на входе - массив, а на выходе - скаляр. Мы хотели бы, чтобы эта функция могла вызываться в формулах рабочего листа, а в качестве фактического параметра ей могли быть переданы как объект Range, так и массив чисел. Вот как я реализовал эту функцию, назвав ее IsMediana: Public Function IsMediana(M As Variant, Cand As Variant) As Integer 'Дан массив M и элемент Cand. В качестве результата возвращается 'разность между числом элементов массива M, больших и меньших Cand. 'Тем самым можно определить близость Cand к медиане массива M. Dim Pos As Integer, Neg As Integer Pos = 0: Neg = 0 'Анализ типа параметра M If TypeName(M) = "Range" Then For i = 1 To M.Rows.Count For j = 1 To M.Columns.Count If M.Cells(i, j) > Cand Then Pos = Pos + 1 ElseIf M.Cells(i, j) < Cand Then Neg = Neg + 1 End If Next j Next i IsMediana = Pos - Neg
ElseIf TypeName(M) = "Variant()" Then 'TypeName is "Variant()" 'Это массив, но не совсем настоящий, для него не определены, 'например, функции границ: LBound, UBound. Dim Val As Variant For Each Val In M If Val > Cand Then Pos = Pos + 1 ElseIf Val < Cand Then Neg = Neg + 1 End If Next Val IsMediana = Pos - Neg Else MsgBox ("При вызове функции: IsMediana(M,Cand)" & _ vbCrLf & "M не является массивом или объектом Range!") End If
End Function
Прокомментируем работу функции IsMediana.
Функции, чьи аргументы имеют универсальный тип Variant, целесообразно строить по принципу разбора случаев. Алгоритм может изменяться в зависимости от типа фактического параметра, задаваемого в момент вызова.
Стандартная функция TypeName(V) возвращает в качестве результата конкретный тип параметра V. Это очень мощная функция VBA, способная разобраться не только с внутренними типами самого языка, но и типами объектов, возвращаемых благодаря Автоматизации (Automation). В частности, функция может определить, имеет ли объект тип Range, File, Sheet или Application.
В процессе работы функции IsMediana(M,Cand) разбираются три возможных случая: M - объект Range, M - массив типа Variant, M имеет любой другой тип.
В случае, когда функция IsMediana вызывается в формуле рабочего листа Excel, то в качестве фактического параметра ей передается объект Range - интервал ячеек этого листа. Следовательно, функция TypeName возвратит строку "Range" в качестве результата. При обработке этого случая организуется цикл по числу строк и столбцов объекта Range, используя свойство Cells этого объекта.
Во втором случае обработка основана на том, что функции передан массив Visual Basic типа Variant. Это возможно, когда при вызове в формуле нашей функции ей передается массив констант. Ниже я приведу примеры подобного вызова. Для таких массивов не определены функции границ UBound и LBound. Поэтому обработка в этом случае основана на использовании цикла For Each.
В третьем случае, когда наш параметр не является ни массивом констант, ни объектом Range, в качестве результата по умолчанию выдается 0. Но выдается также и окно сообщений с предупреждением о возникшей ситуации.
Посмотрим, как это выглядит на экране, и разберем примеры нескольких различных вызовов функции IsMediana в формулах рабочего листа:
увеличить изображение Рис. 2.5. Вызов функции IsMediana в формулах рабочего листа На рабочем листе сформированы два массива: вектор M, вытянутый в виде столбца, и прямоугольная матрица N. Вектор M записан в ячейках C6:C11, матрица N - в F5:I6. В ячейки E8:E16 я поместил формулы, вызывающие функцию IsMediana. Они не являются формулами над массивами, несмотря на то, что параметром может быть массив рабочего листа. Важно, что результат - скаляр. Если бы результат, возвращаемый функцией, был массивом, формулу следовало бы вызывать как формулу над массивами. Для скалярного результата это не так. Хочу обратить внимание на то, что Excel не последователен в этом вопросе. Пользовательская функция, написанная на VBA, аргументы которой являются массивами, но результат которой есть скаляр, может вызываться, как я сказал чуть выше, в обычных формулах рабочего листа. Что касается вызова встроенных функций, то тут ситуация сложнее. Если функции, возвращающей скаляр, передается один массив, то она может вызываться как обычная функция. Если же ей передается несколько массивов, то такой вызов уже должен быть формулой над массивами. Вот классический пример двух вызовов: =СУММ(A1:D1) {=СУММ(A1:D1 +A2:D2)}
Первый вызов работает нормально, а второй должен быть обязательно формулой над массивами, чтобы результатом действительно была сумма всех элементов двух объектов Range. Но вернемся к рассмотрению нашей функции IsMediana. В двух первых вызовах функции IsMediana, записанных в ячейках E8, E9, ей передается в качестве параметров имя массива рабочего листа "M" и разные кандидаты: 7 и 8. Они оба годятся на роль медианы этого массива. В следующих двух вызовах проверяются кандидаты на медиану массива N. Как видите, 4 ближе к медиане, чем 3. Следующие два вызова в ячейках E12 и E13 демонстрируют возможность задания диапазона ячеек в момент вызова, что позволяет, например, работать с частью массива. В двух последующих вызовах вообще не используются в качестве входных данных элементы рабочего листа. Входным параметром M является массив констант, заключенный в фигурные скобки, элементы которого разделяются символом ";". В этих случаях фактический параметр уже не является объектом Range, а имеет тип массива с элементами Variant. Поэтому и функция IsMediana будет работать по-другому, в отличие от предыдущих вызовов. Разбор случаев при данном вызове, зависящий от результата, возвращаемого функцией TypeName, приведет к выбору второго варианта. Наконец, вызов, записанный в формуле ячейки E16, демонстрирует случай, когда входной параметр M - обычное число и, следовательно, не является ни объектом Range, ни массивом. Как следствие, разбор случаев в функции IsMediana приводит к третьему варианту и появлению на экране окна сообщений.
Задача 11 Произведение матриц
Постановка задачи: Найти произведение прямоугольных матриц A*B Из того, что мы узнали ранее, следует, какой вид может иметь заголовок пользовательской функции, решающей эту задачу. Два входных параметра функции должны быть типа Variant. Этот же тип должен быть у возвращаемого функцией значения. Конечно, это не единственно возможное решение. Можно было бы иметь один входной параметр, используя спецификатор ParamArray. Такой способ был бы единственно возможным, если обобщить постановку и попытаться создать функцию, которая должна перемножать произвольное число матриц. Но при умножении двух матриц естественнее иметь и два соответствующих им параметра. Поэтому заголовок получился таким: Function MultMatr(A As Variant, B As Variant) As Variant
Я хочу показать Вам, как написать общую функцию, достаточно широкого назначения. Ее можно будет вызывать в формулах над массивами рабочего листа, передавая ей в качестве фактических параметров A и B массивы рабочего листа (объекты Range). Но не только объекты Range, но и массивы констант будут допускаться в качестве одного или обоих аргументов. Результат работы функции будет записан в массив, выделенный в момент вызова формулы над массивами. Более того, я хочу, чтобы эту же функцию можно было вызывать в обычных функциях и процедурах VBA, передавая в момент вызова массивы VBA в качестве аргументов. Все это, естественно, утяжелит нашу функцию, но позволит мне обсудить отличия "обычных" и "пользовательских функций. С учетом этих замечаний наша функция выглядит так: Public Function MultMatr(A As Variant, B As Variant) As Variant 'Умножение матриц. 'Эта функция может вызываться в формулах рабочего листа Excel. 'В этом случае входные параметры являются объектами Range. 'Функцию можно также вызывать в обычных VBA функциях и процедурах, 'передавая ей в качестве параметров массивы VBA.
Dim AB() As Variant Dim i As Integer, j As Integer, k As Integer Dim N As Integer, M As Integer, P As Integer, Q As Integer Dim Correct As Boolean Dim msg1 As String, msg2 As String Dim Elem As Variant Correct = True
'Определение размерностей матриц If TypeName(A) = "Range" Then N = A.Rows.Count: M = A.Columns.Count ElseIf TypeName(A) = "Variant()" Then N = UBound(A, 1): M = UBound(A, 2) Else: Correct = False End If If TypeName(B) = "Range" Then P = B.Rows.Count: Q = B.Columns.Count ElseIf TypeName(A) = "Variant()" Then P = UBound(B, 1): Q = UBound(B, 2) Else: Correct = False End If ' Проверка корректности задания размерности If Correct And (P = M) Then 'Размерность задана корректно ReDim AB(1 To N, 1 To Q) 'Построение произведения матриц AB =A*B For i = 1 To N For j = 1 To Q Elem = 0 For k = 1 To M Elem = Elem + A(i, k) * B(k, j) Next k AB(i, j) = Elem Next j Next i MultMatr = AB Else 'Некорректно заданы аргументы или размерность If Not Correct Then msg2 = " При вызове MultMatr некорректно заданы аргументы!" _ & vbCrLf & "По крайней мере, один из них не является" _ & vbCrLf & "ни массивом, ни объектом Range" MsgBox (msg2) Else msg1 = " При вызове MultMatr некорректно задана размерность" _ & " перемножаемых матриц!" & vbCrLf & _ "Число столбцов в первом сомножителе = " & M & vbCrLf & _ "Число строк второго сомножителя = " & P MsgBox (msg1) End If End If End Function
Сделаем несколько замечаний.
Из-за того, что фактические параметры могут иметь разную природу, приходится анализировать тип параметра, используя уже упоминавшуюся функцию TypeName.
В зависимости от того, массивом или объектом Range является параметр, по-разному определяются границы массивов.
Если хотя бы один из аргументов не принадлежит ни одному из перечисленных типов, вычисления прерываются с выдачей предупреждающего сообщения.
Еще одна проверка, которую я счел обязательной, - проверка на корректность задания размеров перемножаемых матриц. Конечный пользователь может легко ошибиться и не соблюсти обязательное требование при умножении матриц: "число столбцов матрицы A должно совпадать с числом строк матрицы B". В этом случае результат не будет получен, и будет выдано предупреждающее сообщение. Если же пользователь неверно выделит область памяти под результирующую матрицу, вычисления будут идти. Правда, если эта область урезана по отношению к требуемой, часть результатов будет потеряна. Если же область выделена с избытком, выводятся "лишние" результаты, полученные путем копирования.
Заметьте, сам процесс вычисления результирующей матрицы выполняется одинаково для обоих типов аргументов.
Результат получается в динамическом массиве, который на последнем шаге работы и становится значением функции.
Функцию MultMatr я использовал двояко, - вызывая ее в формулах над массивами в рабочем листе Excel и в обычной процедуре VBA, когда мне понадобилось получить произведение двух матриц, представленных обычными массивами VBA.
Взгляните, как выглядят результаты некоторых экспериментов по умножению матриц на рабочем листе Excel:
увеличить изображение Рис. 2.7. Умножение матриц На рабочем листе я расположил три матрицы разной размерности и дал им имена MatrA, MatrB и MatrC соответственно. Затем, вызывая MultMatr, я получил произведения MatrA*MatrB и MatrB*MatrC, - все выполнилось корректно. Попытка использовать MultMatr для умножения массива констант на матрицу - {1,2; 2,3}*MatrC закончилась неуспехом, поскольку, как я говорил ранее, для массивов констант некорректно работает функция Ubound. При попытке умножения MatrA*MatrC, как и положено, выдалось предупреждающее сообщение о несоблюдении правила размерности перемножаемых матриц.
Задача 12 Решение системы линейных уравнений и обращение матриц
Постановка задачи: Найти решение системы линейных уравнений AX = B Чтобы одним выстрелом "убить двух зайцев", рассмотрим эту задачу в более общей постановке. Будем полагать, что B - это не вектор правых частей, а матрица из m столбцов, каждый из которых задает правую часть уравнения. Тем самым мы будем решать m систем линейных уравнений с одной и той же левой частью и разными правыми частями. Переход от решения одной системы уравнений к решению m систем сам по себе является важным достоинством рассматриваемого алгоритма. Но более важно то, что здесь же становится возможным находить обратную матрицу для A. Достаточно задать в качестве матрицы правых частей B единичную матрицу E, и тогда матрица X будет обратной к A. Заголовок функции, решающей эту задачу, имеет такой же вид, как и при умножении матриц и отличается только именем функции: Function SLEQ(A As Variant, B As Variant) As Variant
Эта функция имеет два входных параметра, - матрицу коэффициентов системы уравнений и матрицу правых частей, и возвращает как результат матрицу X. Конечно, реализация этой функции требует больших усилий, чем умножение матриц. Мне пришлось написать пять процедур, вызываемых в процессе вычислений функции SLEQ. Но сама идея алгоритма достаточно прозрачна. Вначале строится расширенная матрица AB путем присоединения справа матрицы B к матрице A. Затем линейными преобразованиями над ее строками матрица A переводится в единичную матрицу E. Эти же преобразования переводят матрицу B в решение X. Если в качестве B задать единичную матрицу, X будет представлять обратную матрицу. Если B - вектор из одного столбца, то речь идет об обычной системе линейных уравнений. В промежуточном случае B представляет прямоугольную матрицу, и тогда одновременно получа
R± ется решение для m систем линейных уравнений. Теперь приведу текст функции SLEQ и прокомментирую его, а затем уже приведу процедуры, вызываемые по ходу работы. Вот текст основной функции, вызываемой в формуле над массивами рабочего листа Excel: Public Function SLEQ(A As Variant, B As Variant) As Variant 'Решение системы линейных уравнений: AX = B 'A - матрица коэффициентов, B - матрица правых частей. 'X - матрица решений. 'Для каждого столбца B1 матрицы B соответствующий столбец X1 'является решением системы AX1 = B1. 'Если B - единичная матрица E, то X - матрица, обратная к A. Dim i As Integer, j As Integer Dim N As Integer, M As Integer Dim msg1 As String, msg2 As String msg1 = " При вызове SLEQ система уравнений линейно зависима!" msg2 = " При вызове SLEQ некорректно задана размерность системы уравнений!" 'Проверка корректности задания размерности СЛУР. N = A.Rows.Count If (A.Columns.Count = N) And (B.Rows.Count = N) Then 'Размерность задана корректно. M = B.Columns.Count ReDim AB(1 To N, 1 To N + M) 'Построение расширенной матрицы. For i = 1 To N For j = 1 To N AB(i, j) = A.Cells(i, j) Next j Next i For i = 1 To N For j = 1 To M AB(i, N + j) = B.Cells(i, j) Next j Next i 'Цикл линейных преобразований над матрицей AB. Dim k As Integer Dim Independence As Boolean k = 1: Independence = True Do Independence = FindMax(AB, k, i) If Not Independence Then Exit Do Call Change(AB, k, i) Call Normalization(AB, k) Call Linear(AB, k) k = k + 1 Loop While k <= N If Not Independence Then MsgBox (msg1)
Постановка задачи: Найти решение системы линейных уравнений AX = B Чтобы одним выстрелом "убить двух зайцев", рассмотрим эту задачу в более общей постановке. Будем полагать, что B - это не вектор правых частей, а матрица из m столбцов, каждый из которых задает правую часть уравнения. Тем самым мы будем решать m систем линейных уравнений с одной и той же левой частью и разными правыми частями. Переход от решения одной системы уравнений к решению m систем сам по себе является важным достоинством рассматриваемого алгоритма. Но более важно то, что здесь же становится возможным находить обратную матрицу для A. Достаточно задать в качестве матрицы правых частей B единичную матрицу E, и тогда матрица X будет обратной к A. Заголовок функции, решающей эту задачу, имеет такой же вид, как и при умножении матриц и отличается только именем функции: Function SLEQ(A As Variant, B As Variant) As Variant
Эта функция имеет два входных параметра, - матрицу коэффициентов системы уравнений и матрицу правых частей, и возвращает как результат матрицу X. Конечно, реализация этой функции требует больших усилий, чем умножение матриц. Мне пришлось написать пять процедур, вызываемых в процессе вычислений функции SLEQ. Но сама идея алгоритма достаточно прозрачна. Вначале строится расширенная матрица AB путем присоединения справа матрицы B к матрице A. Затем линейными преобразованиями над ее строками матрица A переводится в единичную матрицу E. Эти же преобразования переводят матрицу B в решение X. Если в качестве B задать единичную матрицу, X будет представлять обратную матрицу. Если B - вектор из одного столбца, то речь идет об обычной системе линейных уравнений. В промежуточном случае B представляет прямоугольную матрицу, и тогда одновременно получа
R± ется решение для m систем линейных уравнений. Теперь приведу текст функции SLEQ и прокомментирую его, а затем уже приведу процедуры, вызываемые по ходу работы. Вот текст основной функции, вызываемой в формуле над массивами рабочего листа Excel: Public Function SLEQ(A As Variant, B As Variant) As Variant 'Решение системы линейных уравнений: AX = B 'A - матрица коэффициентов, B - матрица правых частей. 'X - матрица решений. 'Для каждого столбца B1 матрицы B соответствующий столбец X1 'является решением системы AX1 = B1. 'Если B - единичная матрица E, то X - матрица, обратная к A. Dim i As Integer, j As Integer Dim N As Integer, M As Integer Dim msg1 As String, msg2 As String msg1 = " При вызове SLEQ система уравнений линейно зависима!" msg2 = " При вызове SLEQ некорректно задана размерность системы уравнений!" 'Проверка корректности задания размерности СЛУР. N = A.Rows.Count If (A.Columns.Count = N) And (B.Rows.Count = N) Then 'Размерность задана корректно. M = B.Columns.Count ReDim AB(1 To N, 1 To N + M) 'Построение расширенной матрицы. For i = 1 To N For j = 1 To N AB(i, j) = A.Cells(i, j) Next j Next i For i = 1 To N For j = 1 To M AB(i, N + j) = B.Cells(i, j) Next j Next i 'Цикл линейных преобразований над матрицей AB. Dim k As Integer Dim Independence As Boolean k = 1: Independence = True Do Independence = FindMax(AB, k, i) If Not Independence Then Exit Do Call Change(AB, k, i) Call Normalization(AB, k) Call Linear(AB, k) k = k + 1 Loop While k <= N If Not Independence Then MsgBox (msg1)
'Возвращение результата. For i = 1 To N For j = 1 To M AB(i, j) = AB(i, j + N) Next j Next i ReDim Preserve AB(1 To N, 1 To M) SLEQ = AB Else 'Некорректно задана размерность. MsgBox (msg2) End If
End Function
Дам теперь необходимые пояснения:
В реализации функции можно выделить три основные части: построение расширенной матрицы, цикл линейных преобразований и формирование результирующей матрицы. Для построения расширенной матрицы вводится динамический массив AB. Его заполнение достаточно очевидно. Окончательный результат представляет лишь часть расширенной матрицы и формируется на месте исходной матрицы B. Чтобы выделить эту часть, я использую возможности динамического массива: переопределяю его размерность и затем, используя спецификатор Preserve, сохраняю нужные результаты. Таким образом, новый массив не вводится, а сжимается существующий до нужного размера. Правда, это сжатие возможно лишь для последнего измерения. Поэтому предварительно нужные результаты переписываются в начало массива AB.
С содержательной точки зрения основу алгоритма составляет цикл, в котором над матрицей выполняются линейные преобразования. Алгоритм реализует схему Гаусса с выбором главного элемента в столбце. На каждом шаге цикла линейными преобразованиями над строками матрицы ее очередной k-й столбец приводится к единичному столбцу с единицей на диагонали матрицы и остальными нулевыми элементами. Вначале вызывается функция FindMax, которая в k-ом столбце находит максимальный по модулю элемент, расположенный ниже диагонали. Процедура Change меняет строки местами, так чтобы найденный максимальный элемент стал диагональным. Процедура Normalization нормирует k-ю строку делением всех ее элементов на элемент, расположенный на диагонали. Сам этот элемент тем самым становится равным 1. Процедура Linear выполняет линейные преобразования над строками, вычитая из каждой строки k-ю строку, умноженную на подходящий коэффициент, так чтобы сделать нулевыми все элементы k-го столбца, расположенные выше и ниже диагонали.
Вначале проверяется корректность задания размерности матриц A и B. Подобная проверка проводилась при умножении матриц. Вторая проверка позволяет обнаружить линейную зависимость строк или столбцов матрицы A. В случае линейной зависимости система уравнений не имеет единственного решения, а обратную матрицу вычислить невозможно.
Данная функция является чисто пользовательской, и я ориентируюсь лишь на один способ передачи параметров, - в виде Range-объектов.
Приведу теперь тексты процедур, вызываемых в теле функции SLEQ, и начну с функции FindMax: Public Function FindMax(A() As Variant, ByVal Num As Integer, _ Ind As Integer) As Boolean 'В столбце с номером Num матрицы A, начиная с диагонального элемента, 'ищется максимальный по абсолютной величине элемент, 'и вычисляется его индекс Ind. 'Функция возвращает true, если элемент отличен от нуля. Dim i As Integer, Elem As Variant Elem = Abs(A(Num, Num)): Ind = Num For i = Num + 1 To UBound(A, 1) If Abs(A(i, Num)) > Elem Then Elem = Abs(A(i, Num)): Ind = i End If Next i FindMax = Not (Elem = 0)
End Function
Прежде всего, заметьте, что в теле пользовательской функции SLEQ вызываются обычные процедуры и функции. Помимо своей основной задачи - нахождения максимального по модулю элемента столбца и определения его индекса Ind, -булева функция FindMax позволяет определить, является ли система уравнений линейно зависимой. Если найденный элемент равен 0, то и все остальные элементы равны 0, а это и есть признак линейной зависимости. Эта проверка предохраняет нас от возможного деления на 0. Конечно, разумнее выполнять не строгую, а слабую проверку на 0, полагая, что система "почти линейно зависима" (плохо обусловлена), если вычисляемый нами элемент близок к 0. Но это уже вычислительные, а не программистские аспекты, которые здесь обсуждать не место. Следующие две процедуры Change и Normalization совсем простые: Public Sub Change(A() As Variant, _ ByVal Ind1 As Integer, ByVal Ind2 As Integer) 'Перестановка строк с индексами Ind1 и Ind2 матрицы A Dim i As Integer, Elem As Variant If Not (Ind1 = Ind2) Then For i = LBound(A, 2) To UBound(A, 2) Elem = A(Ind1, i) A(Ind1, i) = A(Ind2, i) A(Ind2, i) = Elem Next i End If End Sub
Public Sub Normalization(A() As Variant, Ind As Integer) 'Нормировка строки с индексом Ind матрицы A 'делением на диагональный элемент. Dim i As Integer, Elem As Variant Elem = A(Ind, Ind): A(Ind, Ind) = 1 For i = Ind + 1 To UBound(A, 2) A(Ind, i) = A(Ind, i) / Elem Next i
Постановка задачи: Используя Решатель, найти
Постановка задачи: Используя Решатель, найти решение уравнения: F(x) = a*x^4 + b*x^3 + c*x^2 + d*x + e =0, В задачах 3 и 4 предыдущей главы для нахождения корней этого уравнения я применил метод Ньютона, и сам определял процесс построения решения. Покажу теперь, как воспользоваться Решателем. Наша задача без труда укладывается в общую постановку. Целевая функция - уравнение F(x). Требуется найти такое значение регулируемой ячейки x, чтобы функция F(x) приняла фиксированное значение, равное 0. Никаких ограничений на переменную x не накладывается. Так что все, что нужно, - записать в одну из ячеек рабочего листа переменную x, в другую - формулу, задающую функцию F(x), и вызвать Solver (Решатель) из меню Сервис. Вот как это выглядит на рабочем листе:
увеличить изображение Рис. 2.9. Вызов Решателя для поиска корней нелинейного уравнения Как видите, в диалоговом окне Решателя:
в окне "Установить ячейку цели" (Set Target Cell) задана ссылка на ячейку, содержащую формулу для вычисления значения функции F(x);
включен переключатель "Значение" (Value of) и задано значение 0 в его окошке;
в окне регулируемых ячеек (By Changing Cells) задана ссылка на ячейку, содержащую x; кстати, можно щелкнуть командную кнопку "Догадка" (Guess), и Решатель автоматически попытается сформировать весь список регулируемых ячеек, от которых зависит целевая функция;
так как в нашем случае ограничений задавать не нужно, то больше ничего делать не нужно и можно запустить процесс поиска решения щелчком командной кнопки "Решение" (Solve).
Полученное решение зависит от выбора начального приближения, которое задается в ячейке x перед обращением к Решателю. Вот так выглядит окно Решателя при успешном завершении поиска решения:
увеличить изображение Рис. 2.10. Окно Решателя при успешном поиске решения Наряду с появлением уведомления об успехе поиска в окне "Отчеты" (Reports) предлагается выбрать любые из трех возможных отчетов по результатам работы. Кроме того, можно сохранить полученные результаты или восстановить первоначальные значения. Можно также запомнить созданную постановку задачи. Вот как выглядит отчет "Результаты" (Answer) в нашем случае:
Рис. 2.11. Отчет "Результаты", уведомляющий о результатах решения
Постановка задачи: Используя Решатель, найти
Постановка задачи: Используя Решатель, найти решение системы линейных уравнений AX = B Решение системы уравнений как линейных, так и нелинейных нетрудно сформулировать как оптимизационную задачу в постановке, требуемой Решателем. Пусть имеется система уравнений: F1(X) = 0; F2(X) = 0; … Fn(X) = 0;
Для Решателя эта задача естественным образом формулируется так:
вектор переменных X представляет массив регулируемых ячеек;
функции FI(X) задают ограничения типа равенств;
в качестве целевой функции можно выбрать, например, Ф(X) = SUM(FI(X)). При этом можно выбирать любой критерий для целевой функции, устремляя ее к минимуму, максимуму или требуя нулевое значение для Ф(X).
В качестве примера я использовал те же данные, что и в задаче 12. Взгляните, как выглядит постановка этой задачи в окне Решателя:
увеличить изображение Рис. 2.14. Постановка задачи в окне Решателя А вот как выглядит решение, найденное Решателем:
Рис. 2.15. Решение системы линейных уравнений, найденное Решателем
Постановка задачи: Используя Решатель, найти решение системы линейных уравнений AX = B Решение системы уравнений как линейных, так и нелинейных нетрудно сформулировать как оптимизационную задачу в постановке, требуемой Решателем. Пусть имеется система уравнений: F1(X) = 0; F2(X) = 0; … Fn(X) = 0;
Для Решателя эта задача естественным образом формулируется так:
вектор переменных X представляет массив регулируемых ячеек;
функции FI(X) задают ограничения типа равенств;
в качестве целевой функции можно выбрать, например, Ф(X) = SUM(FI(X)). При этом можно выбирать любой критерий для целевой функции, устремляя ее к минимуму, максимуму или требуя нулевое значение для Ф(X).
В качестве примера я использовал те же данные, что и в задаче 12. Взгляните, как выглядит постановка этой задачи в окне Решателя:
увеличить изображение Рис. 2.14. Постановка задачи в окне Решателя А вот как выглядит решение, найденное Решателем:
Рис. 2.15. Решение системы линейных уравнений, найденное Решателем Нужно понимать, что когда используется Решатель, многое зависит еще и от того, как сформулирована задача. Решатель допускает различные постановки одной и той же задачи, а также имеет ряд параметров, позволяющих управлять процессом решения. Так, при решении системы линейных уравнений можно, например, рассматривать задачу как задачу минимизации следующей функции: Ф(X) = F12 (X) + F22 (X) + … +Fn2 (X) -> Min При минимизации этой функции можно и не задавать ограничения. Понятно, что решение системы исходных уравнений является точкой минимума целевой функции Ф(X). На следующих рисунках показана постановка задачи и ее решение для этого варианта постановки:
увеличить изображение Рис. 2.16. Второй вариант постановки задачи
увеличить изображение Рис. 2.17. Решение, найденное Решателем, во втором варианте Как видите, в обеих постановках Решатель успешно находит решение системы линейных уравнений. Чтобы убедиться, что "проклятие размерности" не так уж и страшно Решателю, я провел еще один эксперимент, выбрав задачу большей размерности, и стал искать решение системы из пяти линейных уравнений. На рабочем листе Excel я задал:
матрицу коэффициентов этой системы - myA;
вектор правых частей - myB;
вектор переменных - SolX5
в ячейках Myl1-Myl5 формулы, задающие соответствующие линейные уравнения. Фактически, я ввел одну формулу в ячейку myl1: {=СУММ(B30:F30*SolX5) -I30}
Затем скопировал ее в соседние ячейки. При копировании автоматически изменяется диапазон ячеек, который указывает на следующую строку матрицы коэффициентов, и изменяется ссылка на ячейку, задающую правую часть уравнения.
В ячейки с именами MYST и MYST1 я ввел формулы для целевых функций, соответствующих двум различным постановкам: =Myl1 + Myl2 + Myl3 + Myl4 + Myl5; = Myl1* Myl1 + Myl2* Myl2 + Myl3* Myl3 + Myl4* Myl4 + Myl5* Myl5;
В первом случае целевая функция линейна, но задаются дополнительные ограничения. Во втором - минимизируется квадратичная функция при отсутствии ограничений. Диалоговое окно Решателя в момент задания его параметров при оптимизации первой целевой функции выглядит так:
увеличить изображение Рис. 2.18. Постановка задачи линейного программирования Обратите внимание на некоторые детали постановки задачи. В окне изменяемых ячеек я задал имя вектора решений - SolX5. Вектор ограничений распространяется на весь диапазон уравнений, но формально задан в виде одного векторного ограничения. Перед тем, как приступить к решению, я изменил настройку параметров Решателя. Взгляните, как выглядит окно Options (Параметры), в котором производится настройка:
Рис. 2.19. Настройка параметров Решателя Как видите, возможностей управлять решением достаточно много, и я еще позже поговорю о них подробнее. Сейчас же замечу, что я оставил без изменения максимальное время, отводимое на решение - 100 секунд, максимальное число итераций - 100, требуемую точность выполнения ограничений - 0, 0000001. Единственное, что я сделал, это включил флажок Linear, указав, что в данной постановке Решатель имеет дело с задачей линейного программирования, поскольку все ограничения и целевая функция линейно зависят от переменных задачи. Точное решение нашей системы уравнений задается вектором (1, 2, 3, 4, 5). Решатель нашел это решение.
увеличить изображение Рис. 2.20. Решение системы 5 линейных уравнений с линейной целевой функцией Я получил также решение этой задачи и для квадратичной целевой функции в различных постановках: с учетом ограничений, без их учета, минимизируя или требуя, чтобы функция принимала нулевое решение. Во всех случаях, поверьте, Решатель не подкачал.
Задача 9 Транспонирование матриц
Постановка задачи: Транспонировать матрицу A: B= AT Наша конкретная задача - транспонирование матрицы на новое место - решается сравнительно просто, поскольку для этого есть стандартная функция ТРАНСП(X). В английской версии и при программировании на VBA она называется Transpose. Можно, например:
ввести прямоугольную матрицу в область рабочего листа, скажем, в ячейки B34:D35;
дать имя этой области, например, "MatrixA";
выделить область ячеек с другой ориентацией для транспонированной матрицы, например A37:B39;
ввести в эту область формулу над массивами: "{=ТРАНСП(MatrixA)}"
Можете убедиться, эти действия приводят к желаемому результату:
Рис. 2.4. Два решения в задаче транспонирования матрицы Хорошо, что есть функция ТРАНСП. Но можно ли справиться с этой классической задачей, не прибегая к вызову стандартной функции? Эта задача имеет различные вариации: матрица может быть квадратной или прямоугольной, транспонировать ее можно на месте (для квадратных матриц) или получать результат, как новую матрицу. При транспонировании на месте возникают циклические ссылки, для прямоугольных матриц появляется проблема различной ориентации исходной матрицы и результата. Но даже в самом простом случае транспонирования квадратной матрицы на новое место при построении собственного решения написать одну или несколько формул в ячейках рабочего листа, решающих эту задачу для матриц произвольного размера, не удается. В отличие от приведенного в предыдущей главе примера с сигнальной матрицей здесь требуется оперировать с индексами элементов, а формулы над массивами этого не допускают. Выход в том, чтобы написать подобную функцию на VBA, вызов которой приведет к желаемому эффекту. Давайте создадим собственную функцию Trans, работающую, как и стандартная функция ТРАНСП. У функции Trans один аргумент. При вызове в качестве фактического параметра ей передается массив рабочего листа (объект Range), заданный в виде диапазона ячеек или своим именем. Этот массив (прямоугольная матрица) транспонируется, и новая матрица возвращается как результат. Она должна быть записана в выделенную в момент вызова область ячеек, отведенных для транспонированной матрицы. Функция Trans, как и функция ТРАНСП, должна вызываться в формуле над массивами и, следовательно, связывается с каждой ячейкой выделенной области. Итак, с программистской точки зрения, нам нужно написать функцию от одного параметра, тип которого должен допускать при вызове массивы рабочего листа (объекты Range) и такой же массив возвращать как результат функции. Каким же должен быть тип параметра и тип функции? Если Вы внимательно следили за предыдущими примерами, то понимаете, что таковым является тип Variant - этот универсальный тип успешно работает в данной ситуации. Остальное - дело техники. Вот текст функции Trans, решающий нашу задачу: Public Function Trans(A As Variant) As Variant 'Функция предназначена для транспонирования массивов 'рабочего листа Excel - объектов Range. 'При работе с переменной A используются свойства объекта Range
Dim B() As Variant Dim i As Integer, j As Integer 'Динамическому массиву В выделяется память ReDim B(1 To A.Columns.Count, 1 To A.Rows.Count) As Variant For i = 1 To A.Rows.Count For j = 1 To A.Columns.Count B(j, i) = A.Cells(i, j) Next j Next i 'Массив B возвращается в качестве результата функции Trans. Trans = B End Function
Нам остается сделать несколько замечаний:
Параметр и функция имеют тип Variant, согласуемый как с массивом, так и объектом Range.
Чтобы получить транспонированную матрицу, нам понадобился динамический массив B. Оператор ReDim в процедуре Trans позволяет массиву получить память, - его размеры можно определить, зная свойства объекта Range, задающего входной массив A.
Операция транспонирования реализуется двойным циклом.
На последнем шаге работы функции ее значением становится сформированный массив B. Он и будет записан в выделенную область памяти рабочего листа.
Заметьте, на предыдущем рисунке показаны оба варианта решения задачи транспонирования матрицы с использованием стандартной функции ТРАНСП и собственной функции Trans. Хочу еще раз обратить внимание на то, чего делать нельзя. Например, в данной задаче многим программистам хотелось бы получать результат в качестве побочного эффекта, введя функцию: ТрансТакНельзя(A As Variant, B As Variant) As Boolean
Эта функция получала бы два массива A и B - входной и результирующий. Транспонированная матрица (массив B) получалась бы как побочный эффект. Результатом работы функции могло бы быть значение True в случае успеха работы и False в противном случае. Такая схема, возможная в других языках и в том числе на VBA недопустима, когда функция вызывается в Excel. Пользовательские функции, вызываемые в формулах ячеек рабочего листа, не допускают побочного эффекта, приводящего к изменению массивов на рабочем листе. Единственная возможность - передать массив как результат пользовательской функции, вызываемой в формуле над массивами. Об этой особенности Excel я уже говорил ранее, но не грех напомнить еще раз об этой важной специфике машины вычислений Excel.
Задача о краске
Мне нравится рассказывать об этой задаче, которая принадлежит В. Очкову, и взята "из жизни" в период середины 90-х годов, когда бартер играл основную роль при расчетах. С тех пор ситуация в экономике существенно изменилась в лучшую сторону, хотя до нормы еще далеко. При описании содержательной постановки я хочу сохранить весь внешний "антураж". Заметьте, цены в те годы примерно на 4 порядка отличались от нынешних. Суть задачи такова. Коллектив программистов под руководством В. Очкова написал программу и продал ее за 14 миллионов лакокрасочному предприятию. "Живых" денег предприятие не платило, но могло в пределах указанной суммы расплатиться краской - своей продукцией. Институт, в котором работал В. Очков, нуждался в краске и готов был оплатить привезенную краску. Краска выпускалась в двух видах тары - больших и малых банках (барабанах), емкость которых составляла 55 и 15 литров, а стоимость пустых барабанов составляла - 30 и 24 тысячи рублей. Литр краски стоит 14 600 рублей. Спрашивается, сколько больших и сколько малых барабанов следует взять, если учесть, что институт заинтересован получить как можно больше краски, а создатель программы скорее заинтересован в том, чтобы как можно меньше денег оставить "лакокрасочникам". Типичная оптимизационная задача! Для решения этой задачи (кстати, это задача с целочисленными переменными) автор использовал Решатель. Благо Excel уже тогда можно было найти повсюду, в том числе и в бухгалтерии лакокрасочного завода. В поставленной Решателю задаче требовалось максимизировать объем получаемой краски при естественном ограничении на сумму имеющихся денег и ограничений целочисленности на искомые переменные - число больших и малых барабанов. Используя установки параметров, принятые по умолчанию, найденный Решателем "оптимальный" ответ гласил, следует взять 2 малых и 16 больших барабанов с краской. Это давало 910 литров краски, но оставляло предприятию 186000 рублей. Возможно, остаток денег был большой, но В. Очков не поверил Решателю и стал искать другой ответ. И нашел лучшее решение. Оно таково: следует взять 15 больших и 6 малых барабанов. При этом краски будет больше (915 л), а денег, заработанных автором, у завода останется значительно меньше (47 000 руб.). Надеюсь, Вы понимаете, что второе решение было получено за счет разумного управления параметрами Решателя. Я напомню, что когда решается задача с целочисленными ограничениями на переменные, вначале ищется решение без учета таких ограничений. Здесь такое оптимальное решение можно найти самому, поскольку понятно, что максимизации объема можно добиться, закупая краску в больших банках. Наша сумма денег позволяет взять примерно 16,8 больших барабанов, т. е. около 924 л. Но брать неполные банки нельзя и, следовательно, нужно искать целочисленное значение. В окрестности найденного оптимального решения Решатель ищет ближайшую целочисленную точку. Значение целевой функции в ней не должно отличаться более чем на величину, заданную параметром Tolerance. Напомним, по умолчанию это значение равно 5%, что в данном примере немало: 46,2 л. Первое решение, выданное Решателем, вполне укладывается в допустимые рамки. Если уменьшить значение параметра Tolerance до 1%, Решатель найдет другое, действительно оптимальное целочисленное решение. История с "оптимальными" решениями имеет продолжение. Рассмотрим еще одно решение с другой целевой функцией, в котором минимизировалась сумма денег, оставляемая заводу. В этом решении следует взять 37 малых и только 6 больших барабанов. Тогда заводу останется только 11 000 руб., так что краска будет закуплена почти на все заработанные автором деньги. Правда, краски будет меньше, - всего 885 литров. Но! Когда это решение было показано тем, кому предназначалась краска, они также признали его наилучшим. С большими барабанами больше возни, и краски на переливах из большого барабана теряется больше. Если ввести дополнительные ограничения на потери, то нормальное решение 37 и 6 может быть получено и при максимизации объема закупаемой краски. Обратите внимание, как, решая, по сути, одну и ту же оптимизационную задачу, можно найти принципиально разные решения. Многое зависит от удачной постановки, выбора критериев, ограничений. Немаловажное значение имеет и умение управлять параметрами алгоритмов оптимизации. Давайте и мы решим эту задачу. Приведу два решения с различными целевыми функциями. Я не буду подробно рассказывать, в какие ячейки помещаются исходные данные, какие формулы записаны в ячейках. Все это, как и результаты, показано на рисунке:
Рис. 2.24. Две постановки и два решения задачи о красках В первой постановке этой задачи максимизируется объем закупаемой краски при ограничении на сумму истраченных денег и требовании целочисленности искомых переменных. Во второй постановке минимизировалась сумма денег, оставляемая заводу. В качестве целевой функции можно выбирать функцию, задающую остаток денег, или рассматривать квадратичную целевую функцию. Ограничения на положительность переменных, их целочисленность и на остаток денег во всех случаях одни и те же. Вот как выглядит окно Решателя в момент постановки задачи, когда минимизируется остаток денег:
увеличить изображение Рис. 2.25. Минимизация остатка денег
Задачи с массивами и программирование
Я хочу теперь сосредоточиться на программном решении классических задач математики, возникающих при обучении программистов. Большинство задач, требующих выполнения операций над матрицами, без программирования не решить. Вместе с тем, библиотека встроенных функций включает основные операции - транспонирование матрицы, умножение и обращение матриц. Для обучения всегда полезно уметь написать собственную реализацию встроенных функций, чем мы сейчас и займемся.
Мир объектов Excel 2000
Адресация ячеек
До сих пор в наших примерах мы использовали так называемую относительную адресацию ячеек таблицы в формате А1. Имя ячейки в этом формате строится из имени столбца (их 256 - A, B,…Z, AB, …AZ,….HZ, IA, …IV) и номера строки (1..65536). Адреса ячеек в этом формате, как мы видели при рассмотрении свойства Range, являются относительными. В этом случае начало координат, задающее нумерацию строк и столбцов, связывается с объектом, вызвавшим Range. Относительные ссылки вещь весьма полезная. Но иногда наряду с относительными ссылками требуются и абсолютные ссылки. Excel достаточно гибок в этом вопросе и предоставляет возможность независимых абсолютных ссылок на строку и столбец. Признаком абсолютной ссылки является знак "$", предшествующий имени строки и (или) столбца. Так что адреса: Z10, Z$10, $Z10, $Z$10 в зависимости от контекста могут именовать одну и ту же или четыре разные ячейки. Адрес ячейки на рабочем листе является лишь частью полного адреса, который, в общем случае, включает адрес листа и адр ес книги. При задании полного адреса имя листа сопровождается знаком "!". Имя книги должно заключаться в квадратные скобки. Эта книга должна быть открыта. Вот пример адресации: 'Example 10 Debug.Print Range("$A$3").Value Debug.Print Range("Лист2!$A$3").Value Debug.Print Range("[BookOne.xls]Лист2!$A$3").Value
В первой строке приведена абсолютная ссылка на ячейку А1 текущего рабочего листа (Лист2), во второй - ссылка на эту же ячейку, но имя включает и имя листа, в третьей - дано полное имя, включающее имя книги.
Для задания адресов ячеек помимо
Для задания адресов ячеек помимо формата A1 можно использовать и другой, так называемый R1C1 - формат. Он называется так, поскольку адрес задается индексом строки (Row) и индексом столбца (Column). И здесь ссылки бывают абсолютными и относительными. В абсолютных ссылках указываются действительно индексы ячейки, в относительных - их смещение по отношению к активной ячейке. Смещение в относительных ссылках задается в квадратных скобках и со знаком, указывающим направление смещения по отношению к активной ячейке. Вот пример, в формулах которого использовались ссылки формата R1C1:
Здесь следует обратить внимание на два обстоятельства:
При вызове Range его параметры можно задавать только в формате А1. Поэтому в левой части мы сохранили "старый способ" адресации. В формулах целесообразнее применять адресацию в формате R1C1, чтобы явно подчеркнуть относительный характер ссылок, задаваемых в формулах.
Если, как часто бывает, вычисления в формулах, распространяются на диапазон, связанный с одним столбцом или одной строкой, то можно задать ссылку, используя только один индекс. В данном примере в последней строке было задано смещение по строкам, поскольку столбец остается неизменным и его можно не указывать
Источники данных и структура объекта Chart
Диаграмма Excel, предоставляя пользователям широкий спектр возможностей по отображению данных, не может быть просто устроена, - она имеет достаточно сложную внутреннюю структуру. Соответственно, такую же сложную структуру имеет и объект Chart. Прежде чем переходить к деталям, давайте рассмотрим общую картину. В диаграмме можно выделить:
общую область, занятую диаграммой. Понятно, что можно менять размеры, внешний вид и другие характеристики общей области.
Область данных, в которой и строится собственно диаграмма.
Источник данных и данные этого источника. Эти объекты играют центральную роль, и о них я расскажу подробнее.
Заголовок диаграммы и другие подписи.
Оси.
Легенду.
Всем этим понятиям соответствуют целый ряд объектов, их свойств и методов. Рассмотрим более подробно источники данных и организацию данных при построении диаграммы. Одним из самых простых и наиболее употребительных источников данных является таблица Excel. Вот как выглядит таблица, используемая для построения диаграмм, показанных на рис. 3.11-3.14:
Рис. 3.15. Таблица, являющаяся источником данных для построения диаграмм Взгляните еще на одну таблицу Excel и диаграмму, для которой данная таблица выступает источником данных:
Рис. 3.16. Диаграмма с источником данных Для того чтобы можно было построить диаграмму, необходимо иметь, по крайней мере, один ряд данных. Напомню специальные термины, применяемые при построении диаграмм. Ось X называются осью категорий и значения, откладываемые на этой оси, называются категориями. Значения отображаемых в диаграмме функций и гистограмм составляют ряд данных. Ряд данных, представляющий последовательность числовых значений, является одним из центральных понятий в построении диаграмм. Этому понятию соответствует объект Series. Поскольку при построении диаграммы, как правило, используется несколько рядов данных, то важную роль играет понятие совокупности рядов данных, которой с объектной точки зрения соответствует коллекция - объект SeriesCollection. Замечу сразу, что все ряды в одной коллекции должны иметь одну и ту же размерность. Excel позволяет строить смешанные диаграммы, где на одной диаграмме одновременно строятся несколько различного типа диаграмм, например, графики и гистограммы. В такой смешанной диаграмме каждому типу соответствует своя группа данных. Группы одной диаграммы составляют коллекцию ChartGroups, элементы которой принадлежат классу ChartGroup. Итак, каждое значение, отображаемое на диаграмме, является членом некоторого ряда данных, ряды объединятся в коллекции. Коллекция рядов связывается с группой, каждая из которых позволяет отобразить диаграмму определенного вида. Группы объединяются в коллекцию групп. В общем случае, каждый ряд данных может иметь свой источник - свое положение, например диапазон ячеек некоторой рабочей книги Excel. Чаще всего, хотя и не всегда, стараются делать так, чтобы у диаграммы был один источник данных, в роли которого выступает таблица Excel. Каждая из таблиц, показанных на двух предыдущих рисунках, служит единственным источником данных для соответствующих диаграмм. Поскольку построенные диаграммы не являются смешанными, то все данные таблицы составляют одну группу - одну совокупность рядов данных. Заметьте, удобно, как это сделано и в наших примерах, чтобы ряды в таблице были именованными. Если нужно задать значения категорий, то они должны составлять первый ряд таблицы, который должен быть неименованным, по крайней мере, в тех случаях, когда он состоит из числовых значений. Это позволяет отличить ряд категорий от обычных рядов данных. Значения категорий можно не задавать, тогда по умолчанию в этой роли выступает начальный отрезок натурального ряда - 1, 2, 3 и так далее. Заметьте также, в качестве рядов данных можно рассматривать как строки, так и столбцы таблицы. Можно построить диаграмму в предположении, что строки таблицы являются рядами данных, а можно полагать, что таковую роль играют столбцы. В наших примерах в первой таблице, отражающей динамику продаж, в качестве рядов использовались именованные столбцы таблицы; фамилии дилеров, осуществляющих продажу, выступают в качестве имен столбцов источника данных. Во второй таблице именованные строки задают ряды данных. Обе таблицы в качестве первого ряда задают значения, располагаемые на оси категорий. Тип диаграммы влияет на ее структуру и предъявляет определенные требования к рядам данных. Так, для построения круговой диаграммы всегда используется только один ряд данных, для трехмерных диаграмм необходимо не менее трех рядов с учетом значений оси категорий. При построении двумерной диаграммы, отображающей графики, показанные на рис. 3.16, используются три ряда данных - один из них задает значения координат на оси X, два других - соответствующий значения двух функций. Хотя детальное обсуждение объектной модели Chart еще предстоит, хочу, предваряя это обсуждение привести пример программного построения диаграммы, где в качестве нескольких источников данных будут использоваться различные диапазоны ячеек рабочего листа Excel, а не единая таблица. По ходу дела в этом примере встретятся многие объекты, упоминавшиеся в этом обзоре, - Series, SeriesCollection и другие. Public Sub CreatingChart() 'Эта процедура строит диаграмму 'Добавить новую диаграмму на лист Dim MySh As Worksheet, MyChO As ChartObject, MyCh As Chart Dim Gr As ChartGroup, SC As SeriesCollection, Sr As Series Dim CG As ChartGroups Set MySh = ThisWorkbook.Worksheets(4) Set MyChO = MySh.ChartObjects.Add(50, 520, 320, 150) Set MyCh = MyChO.Chart
' Добавить ряды данных Set SC = MyCh.SeriesCollection SC.Add Source:=MySh.Range("B26:F27"), RowCol:=xlRows SC.Add Source:=MySh.Range("B29:F29"), RowCol:=xlRows 'Установить тип диаграммы для рядов данных Set Sr = MyCh.SeriesCollection(1) Sr.ChartType = xlColumnClustered Set Sr = MyCh.SeriesCollection(2) Sr.ChartType = xlColumnClustered Set Sr = MyCh.SeriesCollection(3) Sr.ChartType = xlLineMarkers
'Добавить свою ось для графика Sr.AxisGroup = xlSecondary
'Работа с группами Set CG = MyCh.ChartGroups Set Gr = CG(2) Debug.Print CG.Count Debug.Print ; Gr.SeriesCollection(1).Name
'Изменить внешний вид - цвет графика With Sr.Border .ColorIndex = 30 .Weight = xlThin .LineStyle = xlContinuous End With Sr.MarkerBackgroundColorIndex = xlColorIndexNone Sr.MarkerForegroundColorIndex = 30 End Sub
Прокомментируем работу этой программы:
На первом шаге ее работы создается сам объект Chart, представляющий диаграмму, встроенную в рабочий лист. Как обычно, для создания этого объекта вначале создается контейнер.
Теперь задаются ряды данных, и формируется коллекция SeriesCollection. Ряды создаются методом Add этой коллекции. Обратите внимание, для создания трех рядов данных используются два различных диапазона ячеек рабочей книги.
Для каждого ряда задается тип диаграммы, отображающей этот ряд. Для первых двух рядов, задающих планируемые и фактические продажи продукции, выбрано представление в виде гистограммы, для третьего ряда - график, задающий изменение цен.
Стандартные значения оси категорий меняются на названия месяцев, взятые из некоторого диапазона ячеек. Закомментированная строка показывает, что для этой цели может использоваться и массив строковых значений. Здесь также демонстрируется работа с объектом Axis - элементом коллекции Axes, - задающим ось категорий.
Следующий шаг демонстрирует работу с группами. Поскольку создана смешанная диаграмма с двумя разными типами, то и групп будет две. Отладочная печать показывает, что групп действительно две, и ряд, связанный со второй группой имеет имя "Цена".
На заключительном шаге работы изменяется цвет графика и маркеров, отмечающих точки на графике.
В заключение осталось взглянуть, как выглядит построенная диаграмма.
увеличить изображение Рис. 3.17. Программно построенная диаграмма с несколькими источниками данных
В заключение этого параграфа следует сказать, что сегодня Excel интенсивно используется для проведения анализа данных, хранящихся в различных базах и хранилищах данных, в том числе для анализа OLAP - кубов, представляющих многомерные источники данных. Как правило, в этом случае для анализа таких данных в Excel строится сводная таблица. Источником данных для нее являются внешние источники - хранилища данных, а она сама является источником данных для построения сводной диаграммы, интерактивно изменяющейся в процессе работы со сводной таблицей. О сводных таблицах и сводных диаграммах - этом мощном инструменте анализа данных более подробный разговор пойдет в других главах этой книги.
После этих общего обзора перейдем к более детальному рассмотрению диаграмм с объектной точки зрения.
Изменения в объектной модели объекта WorkSheet
Изменения объектной модели не обошли стороной и рассматриваемый нами объект WorkSheet. Многие объекты, встроенные в объект WorkSheet, как, например, уже упоминавшийся объект QueryTable приобрели новые свойства и методы. Два новых свойства появились и у самого объекта Worksheet. Если новое терминальное свойство DisplayRightToLeft вряд ли представляет интерес для российских программистов, поскольку связано с правосторонними языками, то свойство-участник Scripts, возвращающее коллекцию объектов класса Script, представляет несомненный интерес. Каждый элемент этой коллекции задает блок script-кода, используемого в возможных сценариях при публикации рабочей книги в интернет.
Как получить объект Chart
Объект Chart задает диаграмму, расположенную на листе рабочей книги или на отдельном листе диаграммы. В зависимости от типа листа получение этого объекта ведется по-разному. Я уже об этом говорил, но хочу теперь систематизировать приводимые ранее сведения. Рассмотрим все возможные случаи:
Диаграмма встроена в рабочий лист. На рабочем листе может находиться несколько встроенных диаграмм. Коллекция ChartObjects задает совокупность объектов - контейнеров, содержащих эти диаграммы. Зная индекс или имя диаграммы, можно получить доступ к нужному элементу этой коллекции, а свойство Chart позволяет получить объект, задающий диаграмму. Так что вызов ThisWorkbook.Worksheet(3).ChartObjects(1).Chart
вернет объект Chart, задающий первую диаграмму, расположенную на третьем рабочем листе текущей рабочей книги. Я хотел выделить этот объект, вызвав его метод Select, но напрямую метод не вызывается, так что пришлось применить обходной маневр, активизировав вначале контейнер, а затем выделив область диаграммы:
ThisWorkbook.Worksheets(3).ChartObjects(1).Activate ActiveChart.ChartArea.Select
Так что заметьте, в этом случае приходится часто использовать оба объекта - ChartObjects и Chart для решения возникающих задач.
Диаграмма расположена на отдельном листе диаграммы. Специальные листы диаграмм рабочей книги составляют, как уже говорилось, коллекцию Charts. Хочу обратить внимание на два момента. Хотя коллекция представляет собой совокупность специальных листов рабочей книги, элементы этой коллекции являются не столько листами, сколько объектами Chart, задающими диаграммы. Эта ситуация не является типичной для коллекций. Связано это с тем, что на таком листе располагается только один объект Chart. Именно поэтому лист отождествляется с диаграммой. Вызов, который я сделал в отладочном окне Immediate: ThisWorkbook.Charts(1).Select ?ActiveChart.ChartTitle.Text
выделил в текущей рабочей книге первый лист с диаграммой, а, следовательно, сделал активной и саму диаграмму. После чего стало возможным получить заголовок диаграммы. Заметьте, в данной ситуации метод Select работает без проблем. Для получения диаграммы я использовал коллекцию Charts, но заметьте, эта коллекция является частью коллекции Sheets, поэтому тот же результат можно получить, используя эту коллекцию:
Активная диаграмма. Добраться до объекта Chart, задающего активную диаграмму, можно с использованием свойства ActiveChart, как показано в только что приведенных примерах. Еще раз хочу обратить внимание на то, что активизация листа диаграммы приводит и к активизации самой диаграммы. При работе с такими диаграммами можно использовать с тем же успехом и свойство ActiveSheet. Что же касается встроенных диаграмм, то там необходимо предварительно активизировать соответствующий контейнер.
Несколько диаграмм на одном листе диаграммы. Excel позволяет указать один и тот же лист диаграммы, как место расположения нескольких диаграмм. В этом случае новая диаграмма накладывается на область, отведенную первой диаграмме. Руками можно работать с несколькими диаграммами на таком листе, например, изменяя их параметры. Но программная работа возможна только с одним объектом, определяющим первую диаграмму. Более того, если, используя MacroRecorder, записать макрос, повторяющий работу руками с несколькими диаграммами одного листа, то этот макрос не будет корректно выполняться при его запуске. Суммируя это, рекомендую в своей работе придерживаться стратегии - одна диаграмма на листе диаграмм. Это обеспечит корректную программную работу с такими листами.
Коллекция Charts и объект Chart
Напомню, совокупность всех листов рабочей книги задается объектом Sheets. Этот объект, задающий коллекцию, представляет все листы рабочей книги независимо от их типа. Основных типов листов в рабочей книге два - WorkSheet и Chart, соответственно имеются и две коллекции для этих типов листов. Коллекцию WorkSheets мы уже рассмотрели, перейдем теперь к рассмотрению коллекции Charts.
Коллекция Charts
Эта коллекция является частью коллекции Sheets, - ее элементами являются объекты класса Chart, представляющие специальный тип листов рабочей книги - листы, содержащие диаграммы. Всякий раз, когда руками или программно создается диаграмма, ее можно встроить либо в рабочий лист, либо расположить на отдельном листе. Если предпочтение отдается второму варианту, то в коллекции Charts появляется новый элемент. Обратите внимание, все элементы этой коллекции являются объектами Chart, но не всякий объект Chart принадлежит этой коллекции. Коллекция Charts содержит тот же набор свойств и методов, что и коллекция WorkSheets. Она содержит типичный набор свойств: Application, Count, Creator, Parent, Item. Кроме этих свойств имеется менее типичное свойство для коллекций свойство Visible и два свойства VpageBreaks и HpageBreaks, возвращающие одноименные коллекции, элементы которых задают вертикальное и горизонтальное деление листа на страницы, которые могут быть распечатаны. Обо всех этих свойствах я подробно рассказывал при описании коллекции WorkSheets. Все методы, которые есть у коллекции WorkSheets, есть и у коллекции Charts. Вот эти методы: Add, Copy, Delete, Move, PrintOut, PrintPreview, Select. Я их уже описывал, поэтому повторяться не буду. Заметьте, отсутствует только один метод FillAcrossSheets, копирующий диапазон ячеек рабочего листа. Понятно, что для листов диаграмм этот метод неприменим.
Коллекция Workbooks и объект Workbook
Документы, с которыми пользователь работает в Excel, называются рабочими книгами. Каждая рабочая книга представляется объектом WorkBook, а их коллекция - Workbooks.
Коллекция Workbooks
Коллекция Workbooks, содержащая все открытые рабочие книги, имеет обычные для коллекций свойства Application, Count, Creator, Parent и Item. У нее всего 4 метода:
Add([Template]) As Workbook - добавляет новую книгу в коллекцию. Новая книга создается на основе шаблона, заданного параметром Template. Если он опущен, то используется шаблон по умолчанию. Метод возвращает в качестве результата созданную рабочую книгу.
Open(Filename As String, [UpdateLinks], [ReadOnly], [Format], [Password], [WriteResPassword], [IgnoreReadOnlyRecommended], [Origin], [Delimiter], [Editable], [Notify], [Converter], [AddToMru]) As Workbook - открывает существующую книгу. При открытии можно задавать параметры, управляющие свойствами открываемого документа. Обязательным параметром является только первый, задающий имя файла, содержащего открываемую книгу.
Close -закрывает все книги коллекции. При закрытии той или иной книги может появиться диалоговое окно с предложением сохранить сделанные изменения.
OpenText(Filename As String, [Origin], [StartRow], [DataType], [TextQualifier As XlTextQualifier = xlTextQualifierDoubleQuote], [ConsecutiveDelimiter], [Tab], [Semicolon], [Comma], [Space], [Other], [OtherChar], [FieldInfo], [TextVisualLayout], [DecimalSeparator], [ThousandsSeparator]) - открывает текстовый файл, содержащий таблицу, и создает рабочую книгу Excel, преобразуя текстовую таблицу в таблицу Excel. Это бывает важно, если таблицу, подготовленную в одном из текстовых редакторов, нужно импортировать в формат Excel. Обязательным параметром является имя файла, содержащего таблицу, подготовленную в текстовом редакторе. Остальные параметры задают характеристики таблицы. Конечно, для копирования таблицы, подготовленной в редакторе Word, проще использовать буфер и стандартную технику "Копировать" - "Вставить".
Коллекция WorkSheets и объект WorkSheet
Каждая рабочая книга состоит из листов. Совокупность всех листов рабочей книги задается объектом Sheets. Этот объект, задающий коллекцию, представляет все листы рабочей книги независимо от их типа. При программной работе часто полезно иметь дело с подколлекциями коллекции Sheets, содержащими листы только одного определенного типа. Основных типов листов в рабочей книге два - WorkSheet и Chart, соответственно имеются и две коллекции для этих типов листов. К рассмотрению коллекции WorkSheets мы сейчас и переходим.
Коллекция WorkSheets
Эта коллекция является частью коллекции Sheets, - ее элементами являются объекты класса WorkSheet, представляющие рабочие листы - листы электронных таблиц. По умолчанию при создании каждой новой рабочей книги в ее состав включаются три таких листа. С объектной точки зрения это означает, что при создании новой книги автоматически создается коллекция WorkSheets, содержащая три элемента. Как и всякая коллекция в Excel и Office 2000 данная коллекция содержит типичный набор свойств: Application, Count, Creator, Parent, Item. Кроме этих свойств имеется менее типичное свойство для коллекций свойство Visible, которое позволяет сделать видимыми или невидимыми рабочие листы книги. У коллекции WorkSheets есть еще два свойства VpageBreaks и HpageBreaks, возвращающие одноименные коллекции, элементы которых задают вертикальное и горизонтальное деление рабочего листа на страницы, которые могут быть распечатаны. Дело в том, что рабочий лист Excel имеет большие размеры по ширине и длине, так что его полностью нельзя увидеть ни на экране дисплея, ни при выводе на печать. Поэтому при печати часто приходится делить рабочий лист, вставляя разрывы по горизонтали и вертикали. Коллекции VpageBreaks и HpageBreaks содержат объекты, задающие эти разрывы. Методов у коллекции WorkSheets немного, и практически все они типичны для коллекций:
Function Add([Before], [After], [Count], [Type]) As Object - позволяет добавить новый рабочий лист в книгу, возвращая соответствующий объект в качестве результата. Добавленный лист становится активным. Параметры Before и After позволяют указать, куда поместить добавленный лист, - перед или после листа, который до выполнения операции был активным. Параметр Count позволяет одновременно добавить несколько листов, задавая число этих листов. Параметр Type обычно не указывается, ранее он позволял добавлять листы макросов в версии Excel4.
Sub Copy([Before], [After]) - метод Copy вызывается объектом WorkSheets, чаще всего, для создания копии рабочей книги. В этом случае параметры метода не задаются. При копировании отдельной страницы параметры указывают, куда поместить ее копию. Понятно, что только один из этих параметров может быть указан в момент вызова метода.
Sub Delete() - удаляет коллекцию рабочих листов.
Sub FillAcrossSheets(Range As Range, [Type As XlFillWith = xlFillWithAll]) - область, заданная параметром Range, копируется в соответствующее место всех рабочих листов. Тип копирования задается вторым параметром, можно, например, копировать формулы, по умолчанию копируется все содержимое области, заданной параметром Range. Копируемый объект, естественно, должен быть частью одного из рабочих листов коллекции. Вот простой пример, демонстрирующий применение этого метода: Public Sub CopyRange() 'Копирование объекта Range первого листа 'на все листы рабочей книги. With ThisWorkbook .Worksheets.FillAcrossSheets (.Worksheets(1).Range("A7:C11")) End With End Sub
Sub Move([Before], [After]) - используется для перемещения листов. К коллекции его лучше не применять.
Sub PrintPreview([EnableChanges]), Sub PrintOut([From], [To], [Copies], [Preview], [ActivePrinter], [PrintToFile], [Collate], [PrToFileName]) - используются для предварительного просмотра коллекции рабочих листов перед ее печатью и для печати коллекции.
Sub Select([Replace]) - используется для выделения листов коллекции.
Вот еще один небольшой пример на применение методов: Public Sub MoveAndOthers() 'Перемещение листов и другие операции. With ThisWorkbook.Worksheets .Select .PrintPreview (True) 'Метод Move к коллекции лучше не применять! '.Move End With End Sub
Как Вы понимаете, большинство методов - Copy, Move, Select и другие - коллекция WorkSheets "унаследовала" от своих потомков. Чаще всего эти методы применяются к отдельным листам, а не ко всей коллекции в целом. Нам придется еще с ними столкнуться, при рассмотрении методов объекта WorkSheet. Прежде, чем перейти к изучению этого объекта, скажу только, что коллекция WorkSheets, также как и все другие коллекции, событий не имеет.
Методы - "незнакомцы"
Рассмотрим теперь методы, которые нам ранее не встречались. В большинстве случаев эти методы отражают специфику Excel. У рабочего листа их не так и много. Вот эти методы:
Calculate - проводит вычисления формул рабочего листа. Обычно свойство EnableCalculation включено и вычисления идут автоматически. Но при выключенном свойстве необходимо применять этот метод для инициирования вычислений.
ClearArrows - удаляет стрелки трассировки. Эти стрелки можно установить для просмотра зависимостей при вычислениях. Стрелки соединяют ячейки А и В, если формула, записанная в В ссылается на А. Для программного задания трассировки используются методы ShowDependents и ShowPrecedents, но, заметьте, это методы объекта Range, а не рабочего листа. Вот как выглядит трассировка зависимостей, связывающих ячейки при вычислении ряда, задающего вычисление функции ex.
увеличить изображение Рис. 3.7. Отображение зависимостей ячеек при вычислениях в Excel
' Запрос на вычисление функции. Mes = "Задайте функцию и аргумент - получите значение" NameOfCell = InputBox(Prompt:=Mes, _ Title:="Ввод функции", Default:="SIN(3)") Val = Evaluate(NameOfCell) MsgBox ("Значение функции " & NameOfCell & " = " & Val) End Sub
На рисунках показаны окна, которые открывались в процессе диалога с пользователем при вычислении значения выражения:
PivotTableWizard - создает сводную таблицу. Работу со сводными таблицами я рассмотрю в последующих разделах этой книги.
ResetAllPageBreaks - восстанавливает исходную разбивку рабочего листа на страницы, которая возможно была изменена.
SetBackgroundPicture(Filename) - устанавливает графический фон для рабочего листа или листа диаграмм. Картинка для фона берется из файла, имя которого задает параметр FileName.
ShowDataForm - показывает форму данных, связанную с данным рабочим листом. Несколько слов о том, что собой представляет форма данных. Начать нужно, по-видимому, с определения понятия список данных. Excel позволяет связывать с рабочим листом один список данных, представляющий небольшую реляционную базу данных - таблицу, состоящую из именованных столбцов. Форма данных - инструментальное средство для работы с этой таблицей. Форма позволяет добавлять и изменять записи списка. Форма строится автоматически по заголовкам списка и число полей формы совпадает с числом столбцов. Над списком определены разные операции, в частности, возможна фильтрация данных. Но пока при рассмотрении свойств и методов оставим в стороне все, что связано с работой над списком. Об этом предстоит отдельный и подробный разговор.
Методы объекта Application
Методов у объекта Excel.Application меньше, чем свойств, но и их около полусотни. Дадим краткий обзор, опять-таки, объединяя их по возможности в группы:
Метод ActivateMicrosoftApp(Index As xlMSApplication) позволяет активировать приложение Microsoft, заданное соответствующей константой в аргументе метода. Если приложение уже выполняется, то активируется текущий вариант. В противном случае открывается экземпляр приложения, и затем приложение активируется. Константы позволяют задать все основные приложения Office 2000, а также FoxPro, Project и некоторые другие приложения Microsoft.
Группа методов - DeleteCustomList, DeleteChartAutoFormat, AddCustomList, AddChartAutoFormat - позволяет удалять и добавлять пользовательские списки и пользовательские форматы к тем спискам и форматам, которые используются в самом приложении Excel.
Группа из пяти DDE-методов позволяет обеспечить динамический обмен данными между приложениями в соответствии со стандартом DDE. Сохранена для поддержки совместимости с предыдущими версиями Excel.
Методы, запускающие вычисления - Calculate, CalculateFull, приводят к перевычислению рабочих страниц всех рабочих книг. Метод CheckSpelling запускает проверку орфографии во всех рабочих книгах. Метод Evaluate(Name) преобразует имя объекта в сам объект. Эти методы объединяет то, что все они, по существу, являются методами объектов более низкого уровня иерархии - объектов Workbook, WorkSheet, Chat. Объект Application "наследует" эти методы у своих потомков, что позволяет распространять действие метода на все открытые рабочие книги. Следует понимать, что пользоваться вызовом этих методов объектом Application стоит в очень редких случаях. Опять-таки, можно говорить о некоторой излишней перегрузке объекта Application.
Группа Get-методов - GetCustomListContents, GetCustomListNum, позволяет вернуть содержимое пользовательского списка, получить его номер. Методы GetOpenFileName, GetSaveAsFileName позволяют получить имя файла, выбранное пользователем, открывая по ходу дела соответствующее диалоговое окно.
Группа On-методов, позволяющих запустить на выполнение некоторый макрос. Метод OnKey(Key, Procedure) позволяет запустить макрос, заданный вторым параметром метода, при нажатии пользователем комбинации клавиш, заданной первым параметром метода. Метод OnTime(EarliestTime, Procedure As String, [LatestTime], [Schedule]) позволяет запустить макрос, заданный вторым параметром метода, в указанное время. О схожем методе OnTime рассказывалось при описании методов объекта Word.Application. Методы OnRepeat(Text As String, Procedure As String) и OnUndo(Text As String, Procedure As String) позволяют указать макросы и текст, который будет появляться в пунктах "Повторить Выполнение"
и "Отменить Выполнение"
из меню Правка. Когда пользователь выберет соответствующий пункт меню, то запускается макрос, указанный вторым параметром метода. Вот простой пример на применение этих методов:
Public Sub RepeatAndUndo() 'Создание пунктов Повторить и Отменить в меню Правка Call Application.OnRepeat("Hello", "Test") Call Application.OnUndo("7 to A1", "Write7")
End Sub
Public Sub Test() MsgBox ("Hi!") End Sub
Public Sub Write7() Range("A1") = 7 End Sub
Процедура RepeatAndUndo создает соответствующие пункты меню Правка, а процедуры Test и Write7 будут вызываться при выборе пользователем этих пунктов меню. Замечу, что реально особой пользы от применения этих методов не вижу, так как при любых действиях пользователя произойдет обновление этих пунктов меню.
Методы Repeat и Undo близки по духу к рассмотренным только что методам. Они позволяют повторить или отменить последнее действие пользователя при его работе вручную.
Еще одним важным методом, позволяющим запускать макрос на выполнение, является метод Run(Macro, Arg1, Arg2, …). Метод Run позволяет выполнить макрос (процедуру или функцию) проекта рабочей книги или функцию из DLL или XLL. Макрос, запускаемый на выполнение, может находиться в той же рабочей книге, что и макрос, вызвавший метод Run, но может принадлежать и другой рабочей книге. В этом случае, естественно, проекты должны быть связаны по ссылке и в проекте, который вызывает макрос другого проекта, должна быть установлена ссылка на вызываемый проект. При вызове макросу могут быть передано произвольное число аргументов, все они передаются по значению, так что, заметьте, нельзя передать макросу сам объект, а только его значение, задаваемое свойством Value. Метод Run в свою очередь возвращает значение, являющееся результатом выполнения макроса. Приведу простой пример, демонстрирующий все особенности вызова метода Run:
Проекту документа BookOne я дал имя BookOneProject. В этом проекте объявлена глобальная переменная
Option Explicit Public GlobalZ As Variant
В модуль с именем ModuleOne этого проекта я поместил описание процедуры PlusXY и функции Plus1. Они выполняют простые и понятные без комментариев действия.
Public Function Plus1(ByVal X As Integer) As Integer Plus1 = X + 1 End Function
Public Sub PlusXY(ByVal X As Integer, Y As Integer) GlobalZ = X + Y End Sub
В этом же модуле находится и процедура testrun, демонстрирующая вызовы метода Run.
Public Sub testrun() 'Запуск на выполнение функции и процедуры, 'находящихся в том же проекте Dim z As Integer z = Application.Run("Plus1", 7) Debug.Print "z = ", z z = Application.Run("PlusXY", 5, 7) Debug.Print "GlobalZ = ", GlobalZ, "z = ", z End Sub
Вот результаты ее выполнения:
z = 8 GlobalZ = 12 z = 0
В проекте другой рабочей книги Excel с именем BookTwo я установил ссылку на проект BookOneProject и в один из модулей поместил процедуру testrun1, вызывающую макросы проекта BookOneProject:
Public Sub testrun1() 'Запуск на выполнение функции и процедуры, 'находящихся в другом проекте BookOneProject, 'на который установлена ссылка. Dim z As Integer z = Application.Run("BookOneProject.Module1.plus1", 7) MsgBox ("z= " & z) Call Application.Run("BookOneProject.Module1.plusXY", 5, 7) MsgBox ("GlobalZ = " & BookOneProject.GlobalZ) End Sub
И в этом варианте метод Run успешно справляется с вызовом макросов другого проекта. Конечно, в данном примере вместо того, чтобы применять метод Run, можно было бы непосредственно вызвать ту же функцию Plus1. Но, надеюсь, Вы понимаете, что истинная ценность метода Run в том, что имя выполняемого макроса может быть передано ему в качестве параметра, так что в зависимости от ситуации он может запускать разные макросы. Но давайте закончим с примером и вернемся к рассмотрению других методов объекта Excel.Application.
Метод Goto([Reference], [Scroll]) не выполняя макроса, позволяет перейти к его рассмотрению. Другое, может быть, основное назначение метода состоит в том, чтобы перейти в заданную точку рабочей книги Excel. Чтобы перейти к рассмотрению макроса, параметр Reference должен быть строкой, задающей имя макроса. Для перехода в заданную область документа параметр Reference задается объектом Range. Булев параметр Scroll, имеющий значение true, обеспечивает прокрутку области так, чтобы заданная точка находилась в левом верхнем углу области просмотра. Главное, на что стоит обратить внимание, - метод Goto позволяет осуществлять переходы между документами. Вот пример макросов из документа BookTwo, осуществляющих соответственно переходы к заданной области и макросу документа BookOne.
Public Sub GotoRange() 'Переход к заданной области другого документа Application.Goto Workbooks("BookOne.xls").Worksheets("Лист1").Range("A20"), True End Sub
Public Sub GotoMacro() 'Переход к заданному макросу в другом проекте Application.Goto "BookOneProject.Module1.testrun" End Sub
Метод MacroOptions ([Macro], [Description], [HasMenu], [MenuText], [HasShortcutKey], [ShortcutKey], [Category], [StatusBar], [HelpContextID], [HelpFile]) - это еще один метод, связанный с макросами. Он позволяет задать для макроса, указанного первым параметром, различные характеристики - описание, горячие клавиши, раздел справки, связанный с данным макросом, и другие свойства.
Метод RecordMacro([BasicCode], [XlmCode]) - также предназначен для работы с макросами. Он позволяет добавить некоторый программный код в макрос, создаваемый инструментом MacroRecorder. В момент вызова метода MacroRecorder должен быть включен и записывать макрос в модуль, не являющийся активным, другими словами, нельзя произвести запись в тот модуль, макрос которого вызвал метод RecordMacro.
Метод Wait(Time) As Boolean - это последний из описываемых мной методов объекта Excel.Application, входящих в большую группу методов, предназначенных для работы с макросами. Он позволяет организовать задержку вычислений на заданное время, указанное параметром метода. В приведенном ниже примере метод используется, чтобы открыть и показать пользователю некоторую форму, а затем закрыть ее по истечении заданного времени. Этот прием можно использовать в играх, целью которых является проверка внимательности. Вот текст соответствующего макроса:
Public Sub WaitSomeTime() 'Открывает форму на ограниченное время MsgBox ("Форма будет показана на 10 секунд!") FlyForm.Show Application.Wait (Now + TimeValue("0:00:10")) FlyForm.Hide End Sub
Взгляните, как выглядит сама форма.
Рис. 3.1. Форма FlyForm, открытая на "мгновение"
Привожу рисунок этой формы только для того, чтобы пояснить, какая цель преследуется в этом примере. Я предполагал, что при открытии формы пользователь должен успеть в предоставленное ему время ввести два числа в поля X и Y , нажать кнопку, производящую вычисления и запомнить полученный результат. Однако мои намерения не осуществились, и вот по каким причинам. Если форма имеет статус модальной формы, то выполнение макроса приостанавливается до той поры, пока пользователь не закроет форму. Так что в этом случае у пользователя время на работу с формой не ограничено. Это я понимал. Если же форма имеет статус немодальной формы (свойство ShowModal = False), то форма действительно будет открыта в течение 10 секунд. Но в этом случае пользователь не сможет работать с этой формой, вводить значения в поля ввода и нажимать командную кнопку. Хуже всего то, что при попытке ввода значений в поля формы они фактически будут попадать в произвольное место программного текста и порти ть сам проект. Так что следует быть осторожным в подобной ситуации.
Метод Help([HelpFile], [HelpContextID]) позволяет вызвать справочное руководство, указав при необходимости и соответствующий раздел в этом руководстве. Можно вызывать как стандартную справочную систему, - в этом случае не нужно задавать аргументы при вызове метода, либо, что чаще бывает, собственную справочную систему. Первый параметр метода задает имя файла, хранящего справочное руководство. Этот файл может иметь уточнение "chm" , если руководство подготовлено с помощью инструментария HTML Help Workshop, или иметь уточнение "htm", если справочная система создана с помощью инструментария Microsoft WinHelp.
Методы Intersect(Arg1 As Range, Arg2 As Range, …)As Range и Union(Arg1 As Range, Arg2 As Range, …)As Range возвращают в качестве результата объект Range, задающий прямоугольную область, представляющую соответственно пересечение или объединение областей аргументов, которых должно быть не менее двух и не более 30.
Метод InputBox, по существу, эквивалентен одноименной функции из библиотеки VBA [2, стр. 346]и позволяет организовать диалог с пользователем и принять введенное им значение. Функция InputBox является одной из наиболее широко применяемых функций, и примеров ее вызова приводилось достаточно много. Не обойтись без нее и в примерах этой книги. Что вызывать метод InputBox объекта Application или функцию InputBox библиотеки VBA - дело вкуса.
Метод Volatile([Volatile]) позволяет включить или выключить принудительное вычисление для функций, вызываемых в формулах рабочего листа. Метод вызывается непосредственно в функции, которую предполагается пометить. Булев параметр Volatile помечает функцию, как принудительно вычисляемую, если он имеет значение true. Это значение является значением параметра по умолчанию.
Я рассмотрел большую часть методов объекта Application. Замечу, что в предыдущей версии этих методов было значительно больше, поскольку многие функции Excel - математические и прочие были доступны на этом уровне. Теперь, как и положено, все они находятся в специальном контейнере WorkSheetFunction.
Методы объекта Chart
Мы не будем рассматривать методы, которые так или иначе уже встречались. Рассмотрим только основные методы, определяющие новое поведение объекта Chart:
Sub ChartWizard([Source], [Gallery], [Format], [PlotBy], [CategoryLabels], [SeriesLabels], [HasLegend], [Title], [CategoryTitle], [ValueTitle], [ExtraTitle]) Этот метод позволяет построить или модифицировать существующую диаграмму. В отличие от мастера диаграмм (ChartWizard), который вызывается при построении диаграммы вручную, метод не является интерактивным, более того он не позволяет задать все возможные свойства. С его помощью можно выполнить основную работу, а детали строятся с использованием других свойств и методов объекта Chart. В одном из ранее приведенных примеров (процедуре AddChart) я демонстрировал применение этого метода для программного построения диаграммы. Дадим краткое описание параметров метода, все из которых являются необязательными:
Source - объект Range, содержащий исходные данные для построения новой диаграммы. Если параметр опущен, то метод позволяет отредактировать существующую диаграмму - либо выделенную диаграмму рабочего листа, либо диаграмму активного листа диаграмм.
Gallery - задает тип диаграммы и может быть одной из следующих констант: xlArea, xlBar, xlColumn, xlLine, xlPie, xlRadar, xlXYScatter, xlCombination, xl3DArea, xl3DBar, xl3DColumn, xl3DLine, xl3DPie, xl3DSurface, xlDoughnut, или xlDefaultAutoFormat.
Format - задает формат для данного типа диаграммы. Каждому типу диаграммы соответствует некоторое число возможных форматов. Параметр задает номер формата, по умолчанию выбирается первый формат данного типа.
PlotBy - соответствует терминальному свойству PlotBy.
CategoryLabels и SeriesLabels - целые, указывающие число строк или столбцов с метками категорий и рядов данных в области, заданной параметром Source. Указывать эти числа нужно на единицу меньше фактического значения.
Остальные параметры позволяют добавить легенду, задать название диаграммы и ее осей - они совпадают с соответствующими терминальными свойствами.
Sub SetSourceData(Source As Range, [PlotBy]) Устанавливает источник данных диаграммы. Второй параметр соответствует терминальному свойству PlotBy.
Sub ApplyCustomType(ChartType As XlChartType, [TypeName]) Метод позволяет модифицировать диаграмму, применив к ней новый тип - стандартный или настраиваемый. Если этот тип стандартный, то тогда первый параметр полностью его определяет. Его возможные значения совпадают со значениями соответствующего терминального свойства ChartType. Если же тип настраиваемый, то первый параметр должен иметь одно из следующих значений: xlBuiltIn, xlUserDefined или xlAnyGallery. В этом случае второй параметр задает имя типа диаграммы.
Function Export(Filename As String, [FilterName], [Interactive]) As Boolean Позволяет экспортировать диаграмму, преобразуя ее в графический формат. Первый параметр задает имя файла, в который будет записана диаграмма в графическом формате, второй - задает имя графического фильтра в том виде, как оно записано в системном регистре. Булев параметр Interactive должен иметь значение True, если мы хотим вызвать диалоговое окно в процессе фильтрации. Функция Export возвращает значение True в случае успешного завершения работы.
Sub GetChartElement(X As Long, Y As Long, ElementID As Long, Arg1 As Long, Arg2 As Long). Представьте себе, что пользователь щелкнул кнопку мыши где-то над диаграммой. Обработав это событие, можно получить координаты курсора мыши - X и Y. Если теперь вызвать метод GetChartElement с этими координатами, то он вернет значение параметра ElementID - идентификатор элемента диаграммы и значения двух параметров, связанных с этим элементом. Конечно, параметры зависят от типа того элемента, чьи координаты X и Y заданы.
Function Location(Where As XlChartLocation, [Name]) As Chart. Передвигает диаграмму в новое местоположение. Параметр Where имеет следующие значения: xlLocationAsNewSheet, xlLocationAsObject, или xlLocationAutomatic.
В первом случае диаграмма помещается на новый лист диаграммы и параметр Name задает имя этого листа. Во втором случае диаграмма помещается как встроенный объект и Name задает имя рабочего листа. Вот пример, в котором диаграмму, построенную на рабочем листе книги "BookOne" мы переносим на отдельный лист диаграмм:
Public Sub MoveChart() Workbooks("BookOne").Worksheets("Sheet1").ChartObjects(4) _ .Chart.Location Where:=xlLocationAsNewSheet, Name:="Динамика продаж" End Sub
Sub SetSourceData(Source As Range, [PlotBy]) Позволяет установить новый источник данных. Параметры в пояснениях не нуждаются.
В заключение приведем процедуру, создающую трехмерную диаграмму по данным нашего примера с дилерами и продажами:
Public Sub Chart3D() Workbooks("BookFour").Activate With Worksheets("Лист3") Dim myRange As Range Set myRange = .Range("C23:F27") Dim myChart As ChartObject 'Создаем контейнер объекта Chart Set myChart = .ChartObjects.Add(myRange.Left - 100, _ myRange.Top + myRange.Height, 400, 300) End With 'Определяем параметры объекта Chart With myChart.Chart .ChartType = xl3DColumn .SetSourceData Source:=myRange, PlotBy:=xlRows .Location Where:=xlLocationAsObject, Name:="Лист3" End With With ActiveChart .HasTitle = True .ChartTitle.Characters.Text = "Динамика продаж" End With 'Диалог с пользователем If MsgBox("Хотите изменить угол зрения?", vbYesNo) = vbYes Then ActiveChart.RightAngleAxes = True End If End Sub
Дадим некоторые комментарии к этой программе:
В качестве источника данных выступает известная по предыдущим примерам таблица Excel.
Я ввел объекты myRange и myChart класса ChartObjects. Это позволило задать нужные размеры и положение диаграммы, привязанное к таблице данных.
Ранее при построении диаграмм я использовал метод ChartWizard и работу с коллекциями SEriesCollection. Теперь продемонстрирован еще один способ, когда задаются свойства и методы объекта Chart, в частности, для указания источника данных используется метод SetSourceData.
Свойство ChartType позволяет указать тип диаграммы, а метод Location определяет ее, как встроенный объект.
В диалоге с пользователем изменяется угол зрения, задаваемый булевым свойством RightAngleAxes.
Задав еще заголовок диаграммы, и приняв остальные свойства диаграммы по умолчанию, я получил диаграмму, представленную на рис.3.14.
Методы объекта Range
Объект Range имеет около 80 различных методов. Есть ли пересечение с методами объекта Worksheet? Есть, но оно незначительно. Общих методов примерно 10%. К таким методам относятся методы общего назначения: Activate, Calculate, CheckSpelling, Copy, Delete, PasteSpecial, PrintOut, Select. Замечу, что объект Range имеет общие методы не только с объектом Worksheet, но и со старшим в иерархии объектом Workbook. Так описанный ранее метод Run, позволяющий запускать макросы, есть и у объекта Range. Более 20 методов общего назначения входят в следующие группы, которые я лишь назову, не приводя подробного описания:
Clear - 7 методов этой группы проводят разнообразную чистку содержания, комментариев и прочих деталей в области объекта Range.
Copy, Fill - 9 методов этих близких групп выполняют копирование объекта Range и заполнения некоторого диапазона результатами копирования.
Find - 3 метода позволяют проводить разнообразный поиск в области объекта Range.
Show - 4 метода этой группы предназначены для отображения данных на экране дисплея.
Sort - 2 метода выполняют сортировку данных в области объекта Range.
Остается еще более 40 методов, которые я не стану сейчас описывать, надеясь, что большинство из них, по крайней мере, самые важные в работе программиста появятся при рассмотрении большого числа задач в последующих главах. В качестве примере дам краткую характеристику лишь трех из них:
Sub AutoFill(Destination As Range, [Type As XlAutoFillType = xlFillDefault]) - метод из группы Fill позволяет заполнить диапазон, заданный параметром Destination, используя значения объекта Range и тип заполнения, определенный параметром Type. Диапазон назначения Destination должен включать в себя исходный объект Range, вызвавший метод.
Sub AutoFormat([Format As XlRangeAutoFormat = xlRangeAutoFormatClassic1], [Number], [Font], [Alignment], [Border], [Pattern], [Width]) - метод из группы методов форматирования включает автоматическое форматирование диапазона. Тип форматирования определяет первый параметр, остальные параметры имеют булевы и позволяют включать или выключать те или иные возможности форматирования. По умолчанию они включены. Если исходный диапазон представляет одну ячейку, то форматируется весь текущий регион, содержащий эту ячейку.
Sub DataSeries([Rowcol], [Type As XlDataSeriesType = xlDataSeriesLinear], [Date As XlDataSeriesDate = xlDay], [Step], [Stop], [Trend]) - метод из группы методов, предназначенных для работы с данными. Позволяет создать автоматически ряд данных в указанном диапазоне, удовлетворяющий определенному закону построения. Первый параметр имеет два возможных значения: xlRows и xlColumns, указывающих, как будут заполняться данные - по строкам или столбцам. Второй параметр задает тип заполнения ряда данных. Остальные параметры позволяют управлять процессом заполнения данных. Вот небольшой пример на применение этого метода:
Public Sub WorkWD() 'Работа с методом DataSeries Dim Sh As Worksheet Dim myr As Range Set Sh = ThisWorkbook.Worksheets(1) Set myr = Sh.Range("C21:C32") myr.Cells(1, 1) = "31-Jan-2001" myr.DataSeries Type:=xlChronological, Date:=xlMonth Set myr = Sh.Range("D21:D32") myr.Cells(1, 1) = 320 myr.DataSeries Type:=xlDataSeriesLinear, Step:=320
End Sub
Взгляните на два ряда данных, полученных в результате выполнения данной процедуры:
Рис. 3.22. Автоматическое построение рядов данных В следующих главах этой книги, посвященных работе с документами Excel, я рассмотрю по ходу дела применение многих свойств и методов как объекта Range, так и других объектов Excel. А сейчас поставлю точку в рассмотрении объектной модели Excel.
End Sub
Взгляните на два ряда данных, полученных в результате выполнения данной процедуры:
Рис. 3.22. Автоматическое построение рядов данных
В следующих главах этой книги, посвященных работе с документами Excel, я рассмотрю по ходу дела применение многих свойств и методов как объекта Range, так и других объектов Excel. А сейчас поставлю точку в рассмотрении объектной модели Excel.
Дадим теперь краткую характеристику основным методам объекта Workbook. Мы уже говорили о том, что создаются и открываются рабочие книги методами коллекции Workbooks - Add, Open и OpenTextFile. А вот закрываются и сохраняются, используя собственные методы. С них мы и начнем описание методов:
Save, SaveAs, SaveCopyAs - позволяют сохранить рабочую книгу, без ее закрытия и удаления из коллекции Workbooks. При первом сохранении следует применять метод SaveAs, чтобы задать имя файла, в котором книга сохраняется. Метод имеет и другие параметры - формат хранения, пароль, статус и другие характеристики. Последний из этой группы методов создает копию рабочей книги.
Close - выполняет те же функции, что и Save, но одновременно закрывает книгу и удаляет ее из коллекции.
Activate - активизирует рабочую книгу.
Route - направляет рабочую книгу по сети всем участникам совместной разработки. Список участников и другие характеристики задаются в свойствах объекта RoutingSlip. Вот пример процедуры, в которой определяется круг участников работы, после чего им пересылается текущая книга.
Public Sub BookRoute() 'Регистрация исполнителей совместной разработки 'в объекте RoutingSlip. 'Посылка книги совместно работающим исполнителям. With ThisWorkbook .HasRoutingSlip = True With .RoutingSlip .Delivery = xlOneAfterAnother .Recipients = Array("Илья Биллиг", _ "Михаил Дехтярь") .Subject = "Collaboration Test" .Message = "Это книга Excel, пересылаемая в качестве примера" .ReturnWhenDone = True End With .Route End With
End Sub
Заметьте, для того, чтобы при пересылке книги не задавались лишние вопросы, все исполнители, включенные в список Recipients, также как и сам автор документа, должны быть включены в адресную книгу. Согласно установленному порядку книга будет послана первому исполнителю, указанному в списке, и далее будет пересылаться по заданному списком маршруту. После отсылки книги свойство Routed автоматически будет установлено как True.
AcceptAllChanges, RejectAllChanges - принимает или отвергает все изменения, сделанные участниками совместной разработки документа при разделенном доступе.
RefreshAll - обновляет сводные таблицы и все области, содержащие внешние данные.
PurgeChangeHistoryNow (Days,SharingPassword) - удаляет из истории изменений все те, чей срок хранения превосходит число дней, заданных параметром Days. Второй параметр задает общий пароль.
Protect, ProtectSharing, Unprotect, UnprotectSharing - методы, включающие и выключающие пароли личные и общие рабочей книги.
ExclusiveAccess - если книга открыта с разделяемым доступом, то этот метод дает пользователю, вызвавшему его, исключительное право доступа - изменения, сделанные всеми остальными пользователями должны сохраняться в отдельных файлах.
ChangeFileAccess(Mode, WritePassword, Notify) - изменяет статус доступа. Новый статус задается параметром Mode, который может принимать одно из двух значений: xlReadWrite и xlReadOnly. Если файл снабжен паролем и получает статус для записи и чтения, то второй параметр WritePassword задает пароль на запись. Если булев параметр Notify имеет значение True, то пользователь получает уведомление, когда файл недоступен.
AddToFavorites - добавляет в папку Favorites ярлычок рабочей книги.
PivotCaches - возвращает коллекцию областей памяти, отводимых сводным таблицам данной рабочей книги. Элементами этой коллекции являются объекты PivotCache. Каждой сводной таблице - объекту PivotTable отводится своя память (кэш), которую и задает объект PivotCache.
RunAutoMacros - запускает на выполнение все автомакросы данной книги.
LinkSources([Type]), ChangeLink(Name As String, NewName As String, [Type As XlLinkType = xlLinkTypeExcelLinks]), OpenLinks(Name As String, [ReadOnly], [Type]), LinkInfo(Name As String, LinkInfo As XlLinkInfo, [Type], [EditionRef]), UpdateLink([Name], [Type]) - группа методов, позволяющих работать со ссылками. Ссылки могут быть четырех типов:
на другие рабочие книги Excel ,
на документы, связанные по протоколу OLE (например, документы Word, на которые ссылается рабочая книга) или протоколу DDE,
на издателей книги при совместной работе над книгой и ее публикации на сервере,
на подписчиков книги, опубликованной на сервере и доступной для подписчиков.
Метод LinkSources позволяет получить все ссылки типа, заданного его параметром. Если параметр не указан, то будут выданы ссылки на книги Excel. Следующий метод Change позволяет изменить ссылку, Open - открыть документ по заданной ссылке, Info - получить некоторую информацию о документе, Update - обновить ссылки.
Вот пример работы с этой группой методов объекта Workbook:
Public Sub LinkDocs() 'Установление связей рабочей книги Dim BookLinks As Variant Dim i As Integer With ThisWorkbook ' связи с другими книгами Excel. BookLinks = .LinkSources(xlExcelLinks) If Not IsEmpty(BookLinks) Then Debug.Print "Существуют ссылки на рабочие книги Excel!" For i = LBound(BookLinks) To UBound(BookLinks) Debug.Print "Ссылка" & i & " : ", BookLinks(i) If BookLinks(i) = "BookTwo" Then .OpenLinks BookLinks(i) .ChangeLink BookLinks(i), "BookOne" End If Next i Else: Debug.Print "Ссылки на рабочие книги отсутствуют!" End If
BookLinks = .LinkSources(xlOLELinks) If Not IsEmpty(BookLinks) Then Debug.Print "Существуют ссылки на OLE - документы!" For i = LBound(BookLinks) To UBound(BookLinks) Debug.Print "Ссылка" & i & " : ", BookLinks(i) Next i If .LinkInfo("Word.Document.8|E:\O2000\Remarks.doc!'", _ xlUpdateState, xlOLELinks) = 1 Then Debug.Print "Автоматическое обновление данных с OLE - документами!" End If Else: Debug.Print "Ссылки на OLE - документы отсутствуют!" End If
BookLinks = .LinkSources(xlPublishers) If Not IsEmpty(BookLinks) Then Debug.Print "Существуют ссылки на издателей документа!" For i = LBound(BookLinks) To UBound(BookLinks) Debug.Print "Ссылка" & i & " : ", BookLinks(i) Next i Else: Debug.Print "Ссылки на издателей отсутствуют!" End If
BookLinks = .LinkSources(xlSubscribers) If Not IsEmpty(BookLinks) Then Debug.Print "Существуют ссылки на подписчиков документа!" For i = LBound(BookLinks) To UBound(BookLinks) Debug.Print "Ссылка" & i & " : ", BookLinks(i) Next i Else: Debug.Print "Ссылки на подписчиков отсутствуют!" End If End With End Sub
Вот как выглядят результаты отладочной печати, полученные при работе этой процедуры:
Существуют ссылки на рабочие книги Excel! Ссылка1 : BookTwo Ссылка2 : E:\O2000\DsCd\Ch11\BookOne.xls Существуют ссылки на OLE - документы! Ссылка1 : Word.Document.8|E:\O2000\Remarks.doc!' Автоматическое обновление данных с OLE - документами! Ссылки на издателей отсутствуют! Ссылки на подписчиков отсутствуют! Новые методы объекта Workbook
У объекта Workbook в Excel 2000 появились два новых метода:
Sub ReloadAs(Encoding As MsoEncoding),
Sub WebPagePreview().
Оба метода, так или иначе, связаны с общей тенденцией публикации документов Excel в Интернет. Рабочие книги, публикуемые в Интернет и интранет, хранятся, естественно в формате HTML. При их чтении могут возникнуть проблемы с кодировкой. Метод ReloadAs(Encoding As MsoEncoding) позволяет перезагрузить книгу в формате HTML, используя нужную кодировку, заданную параметром метода, значением которого может быть, например, константа msoEncodingCyrillic. Метод WebPagePreview позволяет перед публикацией книги отобразить ее на дисплее в том виде, как будет выглядеть соответствующая Web-страница, открываемая в интернет для работы с рабочей книгой.
Методы объекта Workbook предназначены, как можно видеть, для выполнения общих операций над документом и по существу не определяют специфических для Excel действий. Чтобы познакомиться со спецификой, следует пойти вглубь иерархии объектов.
Методы объекта WorkSheet
У объекта WorkSheet методов достаточно много. Часть из этих методов применима ко многим объектам и уже встречалась или еще будет встречаться при описании других объектов. Так что введенные в [3] понятия общности и схожести применимы не только к самим объектам, но и их отдельным свойствам и методам. Поэтому я разделю описание методов на две группы, и начну с более простой группы схожих методов.
Методы - свойства
Теперь я хочу рассмотреть еще несколько важных методов объекта WorkSheet, которые я выделил в отдельную группу. Эти методы похожи на свойства. В результате их работы возвращаются объекты. По-видимому, правильно считать, что возвращаемые объекты непосредственно вложены в объект WorkSheet и определяют его структуру также как объекты, возвращаемые свойствами-участниками. Вот почему я называю эти методы свойствами. В эту группу методов входят:
Function ChartObjects([Index]) As Object- возвращает коллекцию ChartObjects. Если задан параметр Index, то возвращается элемент этой коллекции - объект ChartObject. Возможный параметр Index задает номер или имя возвращаемого объекта. Заметьте, элементами коллекции являются объекты ChartObject, а не объекты Chart. Объект ChartObject является контейнером объекта Chart. Его методы и свойства позволяют управлять внешним видом и размерами встроенной в контейнер диаграммы. Чтобы получить сам объект Chart, следует воспользоваться свойством Chart объекта ChartObject. Не следует путать метод ChartObjects со свойством Charts объекта WorkBook, которое возвращает коллекцию Charts, представляющую страницы с диаграммами рабочей книги. Я напомню, что в Excel диаграммы могут быть встроены в обычный рабочий лист и, следовательно, с объектной точки зрения быть встроенными в объект WorkSheet. С другой стороны, диаграммы могут располагаться на отдельных листах рабочей книги. Такие специальные листы для отображения диаграмм и составляют коллекцию Charts. Элементы этой коллекции - объекты Chart - представляют либо встроенные диаграммы, либо листы с диаграммами. Согласно справочной системе Excel объект Chart, задающий лист с диаграммой, также имеет метод ChartObjects, возвращающий коллекцию контейнеров. Однако, практически работать с этой коллекцией не удается, да и в этом нет никакой необходимости, поскольку сам объект Chart задает и диаграмму, расположенную на листе. Наличие объектов Chart и ChartObject, их коллекций, большого числа различных свойств и методов, возвращающих эти объекты, создает впечатление излишней сложности. Приведу сейчас два примера, демонстрирующих работу с этими объектами:
Public Sub WorkWithCharts() 'Работа с встроенными диаграммами Dim CHO As ChartObjects 'коллекция контейнеров Dim ChO1 As ChartObject 'контейнер диаграммы Dim Ch1 As Chart 'встроенная диаграмма With ThisWorkbook Set CHO = .Sheets("Лист2").ChartObjects Set ChO1 = CHO(2) 'Меняем внешний вид диаграммы ChO1.RoundedCorners = True ChO1.Select Debug.Print ChO1.Name 'Получаем диаграмму Set Ch1 = ChO1.Chart Ch1.HasTitle = True Ch1.ChartTitle.Text = "Заказы Февраля" Debug.Print Ch1.Name 'Работа с листами диаграмм Dim Ch2 As Chart, Ch3 As Chart Dim ChO2 As Object Set Ch2 = .Charts(1) 'Лист диаграммы Ch2.HasTitle = True Ch2.ChartTitle.Text = "Заказы Марта" 'Контейнер для листа диаграммы Set ChO2 = .Charts(2).ChartObjects 'Работать с этим контейнером практически невозможно! 'Но особой необходимости в этом нет. 'Set Ch3 = ChO2.Chart 'Ch3.ChartTitle = "Заказы Апреля" End With
End Sub
В этом примере я работаю вначале с рабочим листом, на котором расположены две диаграммы. Получив контейнер одной из этих диаграмм - объект ChartObject, я меняю внешний вид диаграммы, закругляя ее края. Затем получаю саму диаграмму - объект Chart и задаю свойства этого объекта, определяя заголовок диаграммы. Попытка аналогичным образом работать с диаграммой, расположенной на отдельном листе, не увенчалась успехом из-за невозможности получить контейнер. В этом случае необходимо работать с самим объектом Chart - элементом коллекции Charts.
Следующий пример носит более содержательный характер. В нем вначале программно создается уже не раз упоминавшаяся последовательность чисел Фибоначчи, а затем программно строится диаграмма (график), отражающая рост этих чисел с изменением их порядкового номера. Диаграмма строится в три приема - создается контейнер, затем объект Chart , затем вызывается метод ChartWizard, который и осуществляет построение диаграммы. Вот текст соответствующей процедуры:
Public Sub AddChart() 'Формируется последовательность чисел Фибоначчи. 'Вставляется диаграмма, отражающая график роста этих чисел. Dim myRange As Range Dim MySh As Worksheet Dim CHOS As ChartObjects Dim CHO As ChartObject Set MySh = ThisWorkbook.Worksheets(3) With MySh Set myRange = .Range("A1") With myRange .Value ="Числа Фибоначчи" .Offset(1, 0).FormulaR1C1 = "0" .Offset(2, 0).FormulaR1C1 = "1" .Offset(3, 0).FormulaR1C1 = "=R[-2]C +R[-1]C" .Offset(3, 0).Select Selection.AutoFill Destination:=Range("A4:A10"), _ Type:=xlFillDefault End With 'Добавление диаграммы Set CHOS = .ChartObjects Set CHO = CHOS.Add(50, 50, 250, 200) CHO.Chart.ChartWizard Source:=.Range("A2:A10"), _ Gallery:=xlLine, Title:="Числа Фибоначчи" End With
End Sub
В результате работы этой процедуры соответствующий рабочий лист Excel имеет вид:
Рис. 3.10. Программно построенная диаграмма
Function PivotTables([Index]) As Object возвращает коллекцию PivotTables. Если задан параметр Index, то возвращается элемент этой коллекции - объект PivotTable. Возможный параметр Index задает номер или имя возвращаемого объекта. Объект класса PivotTable определяет сводную таблицу. Эти таблицы играют важную роль при представлении итоговых данных и формировании отчетов. Но о них есть смысл говорить после знакомства с базами данных. Отметив наличие такого объекта, отложив его обсуждение до той поры, пока не встретимся с ним в нужном месте и в нужное время.
Function Scenarios([Index]) As Object возвращает коллекцию Scenarios. Если задан параметр Index, то возвращается элемент этой коллекции - объект Scenario. Возможный параметр Index задает номер или имя возвращаемого объекта. Элемент класса Scenario представляет сценарий, используемый при анализе данных электронной таблицы. Я рассмотрю подробно применение сценариев на примерах в последующих главах книги.
Function OLEObjects([Index]) As Object возвращает коллекцию OLEObjects. Если задан параметр Index, то возвращается элемент этой коллекции - объект OLEObject. Возможный параметр Index задает номер или имя возвращаемого объекта. Элемент класса OLEObject представляет OLE-объект, встроенный в рабочий лист.
Объект Chart
Трудно перечислить все достоинства Excel. Конечно, на первом месте стоит возможность работы с данными, предоставляемая электронной таблицей Excel, его машиной вычислений и мощной библиотекой встроенных функций. Но на второе место по важности, несомненно, претендуют возможности Excel по графическому отображению данных. Для этого используются диаграммы Excel, позволяющие отображать одни и те же данные в самых различных форматах в зависимости от потребностей пользователя. Excel предоставляет самые широкие возможности для варьирования формой представления данных. Диаграммы могут быть плоскими и объемными, двумерными и трехмерными, круговые и цилиндрические, данные можно отображать в виде графиков или гистограмм различного типа. На одной диаграмме может отображаться несколько групп, где каждая группа содержит один или несколько рядов данных (серий), отображаемых в одном формате. Диаграммы выделяются цветом, имеют оси, сопровождаются заголовком, подписями, легендой. Легенда - это надпись на предмете, например, на монете. На диаграммах, содержащих несколько рядов данных, легенда задает название каждого ряда. Видов диаграмм в Excel великое множество. По типу диаграммы делятся на стандартные и нестандартные или настраиваемые (Custom). Стандартных типов - 14, но каждый из них имеет до десяти форматов, так что в общей сложности их около 100. Настраиваемые типы в свою очередь разделяются на встроенные и определенные пользователем. С их помощью на одной диаграмме можно задать комбинацию нескольких стандартных типов. Мы в этой книге не собираемся останавливаться на подробном разборе всех типов, поскольку для программистов, в основном, это понятные вещи. Но несколько слов все-таки сказать следует. По сути, диаграммы предназначены для отображения графиков функций и гистограмм. Если по точкам строится график функции Y=F(X), то, как известно, необходимо задать два множества - аргументов и значений. Например, функция, определяющая объем продаж, осуществленных ее дилерами в первом квартале, имеет аргументами фамилии дилеров (Иванов, Петров, Сидоров), а значениями - объем продаж, выраженный в рублях (100, 200, 150).Множество значений называется в Excel рядом данных, а аргументы называются категориями. Соответственно, ось X (аргументов) называется осью категорий, а ось Y - осью значений. При построении графика можно опустить задание аргументов и тогда по умолчанию за их значения принимается начало натурального ряда чисел - 1, 2, 3 и т.д. Очень часто на одной диаграмме отображается несколько графиков. Если эти графики отражают некоторую тенденцию, например динамику объема продаж во времени, то лучше использовать трехмерную диаграмму, в которой появляется третья ось, чаще всего это ось времени. В Excel она называется осью рядов данных, так как фиксирует изменения ряда данных. Понятие гистограммы пришло из теории вероятности. Можно считать, что гистограмма отличается от графика функции тем, что аргументами при построении гистограммы являются интервалы, и значение функции связывается с интервалом. Гистограмма, обычно, отображается в виде прямоугольников, каждый из которых имеет в основании заданный интервал, а высотой - значение функции. В примере с дилерами и продажами, естественно отобразить данные в виде гистограммы, поскольку объемы продаж в реальности связаны с интервалами времени (1-й квартал, 2-й, 3-й, 4-й). Поскольку речь идет о графических объектах, то лучше на них взглянуть. На рис. 3.11 - 3.14 показаны различные диаграммы, отражающие данные с дилерами и продажами нашего простенького примера. На первом из этих рисунков показана классическая гистограмма, чаще всего применяемая в подобных ситуациях. На втором рисунке те же данные представлены в виде графиков. Конечно, чаще всего графики используются в инженерных расчетах и математических вычислениях. В экономических задачах предпочтительнее более наглядные формы отображения данных, например, круговые диаграммы, одна из которых показана на третьем рисунке. Наконец, на четвертом рисунке этой серии показана трехмерная диаграмма. Третье измерение - это, чаще всего, ось времени, что позволяет наглядно отобразить динамику объема продаж. Представленные рисунки отражают лишь малую долю тех возможностей, которые предоставляют диаграммы Excel
Рис. 3.12. Представление данных о продажах в виде графиков
Рис. 3.13. Круговая диаграмма, отражающая вклад каждого дилера
увеличить изображение Рис. 3.14. Трехмерная диаграмма, отражающая динамику продаж
Объект Excel Application
Объект Excel.Application задает приложение Excel. А посему свойства, методы и события этого объекта должны характеризовать приложение в целом. Понятно, что у этого объекта должно быть свойство Workbooks, возвращающее все открытые в приложении рабочие книги, свойство Windows, возвращающее открытые окна, свойства, такие как CommandBars, возвращающие объекты интерфейса, и другие подобные свойства. Методов и событий, характерных для всего приложения в целом, по-видимому, не так уж и много. Так что, казалось бы, структура этого объекта должна быть достаточно простой. Однако реально это не так, - у объекта Excel.Application очень большое число свойств, методов и событий, что не позволяет мне описать их полностью, да и нет в этом особого смысла. Объект Excel.Application, на мой взгляд, явно перегружен, многие его свойства и методы без всякого ущерба можно было бы исключить, поскольку они оперируют с объектами, стоящими на более низких уровнях иерархии и не имеют прямого отношения ко всему приложению в целом. Приведу лишь один пример. Первое по алфавиту свойство ActiveCell возвращает объект, задающий активную ячейку. Понятно, что речь идет об активной ячейке активной страницы активной рабочей книги. Непонятно только, зачем нужно было добавлять это свойство самому приложению. Вполне достаточно, чтобы им обладал объект WorkSheet, задающий страницу книги. Более того, если в момент вызова свойства ActiveCell нет активной страницы с ячейками, то возникнет ошибка, чего не происходит, если активную ячейку вызывает объект WorkSheet. Примеров подобной перегруженности объекта Application можно привести много. Я в своем описании объектов верхнего уровня не всегда буду упоминать такие свойства, полагая, что лучше рассказать о них там, где они необходимы по существу.
Объект Workbook
Рабочая книга Excel устроена проще, чем документ Word. Как и положено книге, она состоит из страниц (листов). В терминах объектов это означает, что объект Workbook имеет свойство Sheets, возвращающее объект Sheets - коллекцию листов рабочей книги. Поскольку рабочие книги Excel содержат листы разного типа, то наряду с коллекцией Sheets у объекта Workbook имеются свойства, возвращающие коллекции листов разного типа:
Worksheets - рабочих листов;
Charts - листы, содержащие диаграммы;
Excel4MacroSheets - листы с макросами одной из предыдущих версий Excel. Реально листы такого типа уже не встречаются. Поэтому и в документации они не всегда упоминаются. Я о них также больше говорить не буду. Здесь отдана дань прошлому, когда макросы располагались на листах рабочей книги. Сейчас все макросы находятся в модулях программного проекта.
Эти коллекции и составляют в совокупности коллекцию Sheets. Чаще всего приходится работать с каждой из этих коллекции в отдельности, но иногда полезно иметь возможность выполнять операции над всеми листами, независимо от их типа. С некоторым сожалением отмечу, что отсутствует класс Sheet, обобщающий свойства классов Workbook и Chart. Теперь, когда состоялось первое знакомство с основными объектами, составляющими рабочую книгу, давайте перейдем к более систематическому обзору всех свойств- участников объекта Workbook.
Объект WorkSheet
Объект Worksheet - рабочий лист является элементом коллекции Worksheets. Он представляет основной тип страниц рабочей книги. Именно на этих страницах разворачиваются основные действия в ячейках электронной таблицы. Основу рабочего листа составляет прямоугольная таблица ячеек. Главная особенность электронной таблицы состоит в том, что в ее ячейки можно вводить не только данные, но и формулы. Формулы Excel, также как и обычные математические формулы, также как и выражения в языках программирования, оперируют при вычислении значений константами, переменными и функциями. В электронной таблице роль переменных играют ячейки таблицы. Существует некоторый алгоритм, определяющий порядок, согласно которому вычисляются формулы в ячейках электронной таблицы. При изменении данных таблиц, инициированных пользователем, внешними ссылками или выполнением макросов программного проекта, пересчитываются и формулы. Это делает таблицу живой, - изменение значения одной ячейки приводит, возможно, к пересчету всей таблицы. Также как для документов Word работа с текстом является главным занятием пользователей, работающих с документом, так и работа с ячейками - ввод данных и формул в ячейки, и, тем самым, инициирование вычислений лежит в основе работы с рабочим листом. С объектной точки зрения отдельные ячейки электронной таблицы и области, содержащие совокупности этих ячеек, то, что называется объектами Range, являются основными объектами рабочего листа. Но, естественно, рабочий лист состоит не только из объектов Range, есть и другие компоненты. Объектная модель рабочего листа Worksheet достаточно сложна, что отражает, впрочем, сложность изучаемого объекта. Давайте познакомимся с ней поближе.
Объектная модель Excel
Прежде всего, несколько слов о том, как устроена объектная модель Excel и других приложений Office 2000. В этой модели объекты связаны между собой отношением встраивания. На нулевом уровне иерархии существует некоторый центральный объект, в который встроены другие объекты, составляющие первый уровень иерархии. В каждый из объектов первого и последующих уровней могут быть встроены объекты следующего уровня. Так это процесс продолжается. Таким образом, объекты в этой модели "толстые", поскольку в них встроено большое число других объектов. В особенности это касается объектов, стоящих на верхних уровнях иерархии. Формально встраивание реализуется с помощью свойств объектов. Свойства могут быть как терминальными, не являющимися объектами, и так называемыми свойствами - участниками, которые возвращают объекты при их вызове. Давайте перейдем к рассмотрению библиотеки объектов Excel 9.0 и начнем с центрального объекта этой библиотеки - Excel.Application.
Объекты Range и Selection
Объекты Range и Selection относятся к группе схожих объектов, встречающихся в различных приложениях Office 2000. Это основные объекты, с которыми приходится работать программисту. В приложении Word есть достаточно четкая логика в том, как создаются объекты Range. Объекты верхнего уровня, например, Document, имеют метод Range, позволяющий создать новый диапазон. Объекты более низкого уровня, например Paragraph, имеют свойство Range, возвращающее диапазон, связанный с объектом. В Excel ситуация другая. Все объекты Excel имеют только свойство Range. Это свойство имеют и верхний в иерархии объект Application и Worksheet и сам объект Range, представляющий объекты нижнего уровня вплоть до ячейки. Синтаксис этого свойства следующий: Property Range(Cell1 [,Cell2]) As Range
С объектом Selection тоже дело обстоит не так просто как в приложении Word, поскольку в Excel нет класса объектов Selection. Объект Selection возникает двояко - либо в результате работы метода Select, либо при вызове свойства Selection. Тип полученного объекта может быть различным и определяется типом выделенного объекта. Чаще всего, объект Selection принадлежит классу Range и тогда при работе с ним можно использовать все свойства и методы объектов класса Range. Вернемся к объекту Range, создаваемому, чаще всего, при вызове свойства Range тех или иных объектов Excel. Что может быть задано в качестве параметров Cell1 и Cell2? Давайте рассмотрим этот вопрос подробнее. Прежде всего, нужно понимать, что Range уникальный объект - он может представлять как единственную ячейку таблицы, так и столбец или строку, некоторую связную и не связную прямоугольную область, а также объединение и пересечение всех подобных элементов. Это же касается и объекта Selection. Параметры Cell1 и Cell2 это не просто имена ячеек таблицы. Они имеют непростой синтаксис и в общем случае могут быть достаточно сложными выражениями, позволяющими соответственно вернуть объект Range сложной конфигурации. Если при вызове используется только один параметр, то Cell1 может быть:
Именем ячейки, например, - "A1"
Диапазоном ячеек, например, - "A1: B5"
Выражением над диапазонами, содержащим операции объединения (запятая) и пересечения (пробел), например, - "A1:B5, F1: G8" или "A1:B5 A3:G8"
В случае, когда задаются оба параметра - Cell1 и Cell2, то они определяют прямоугольную область, заданную наименьшим левым верхним углом и максимальным правым углом диапазонов, определяемых параметрами. В этом случае параметры могут быть и переменными класса Range.
Давайте начнем с простых примеров:
Public Sub WorkWithRS() 'Работа с объектами Range и Selection 'Example 1 Workbooks("BookOne").Activate Worksheets("Лист2").Activate Range("A3") = 5 Range("A4") = "=A3+2" Range("A5:A6") = "=A3+A4"
End Sub
Обратите внимание на следующие моменты:
Здесь при вызове Range во всех случаях используется только один параметр - и это ячейка или диапазон, заданные в формате "А1".
Цепочка вызовов начинается непосредственно с Range, по умолчанию это означает, что речь идет о свойстве Range активного листа. Заметьте, что это должен быть рабочий лист, иначе возникнет ошибка. Так что для полноты картины следовало писать, например Worksheets("Лист2").Range или ActiveSheet.Range.
Объекту Range мы присваиваем значения и формулы. Опять - таки здесь используется концепция умолчания, в результате присваивания определяются свойства Value или Formula объекта Range. По-видимому, лучше писать подобные присваивания в таком виде: 'Example 2 Range("B1").Value = 7 Range("B2").Formula = "=B1+2" Range("B3:B4").Formula = "=B1+B2"
Когда формула присваивается диапазону ячеек, то переменные в формуле носят относительные имена и изменяются при переходе к очередной ячейке диапазона, так что формула, приписанная ячейке A6, будет иметь вид: "=A4 +A5"
Следующий пример демонстрирует важную еще одну важную для понимания относительность ссылок, задаваемых параметром Cell. Этот параметр задает ссылки, относительно объекта Range, вызвавшего свойство Range. Вот пример:
'Example 3 Dim myRange As Range Set myRange = Range("C1:C4") myRange.Range("A1") = 7 myRange.Range("B1") = 7 myRange.Range("A2") = "=A3+2" myRange.Range("A3:A5") = "=A3+A4"
Вначале создается объект myRange, заданный диапазоном "C1:C4". Вызов myRange.Range("A1") определяет объект из одной ячейки А1, где адрес вычисляется относительно объекта myRange. Такая ссылка задает ячейку С1 в абсолютных адресах. Заметьте, что можно обращаться к любым ячейкам вне зависимости от того, какую область занимает вызывающий объект myRange, задающий по существу начальную точку отсчета. В нашем примере присваивается значение не только ячейкам A1 - A4, но и ячейкам B1 и A5, не входящих в диапазон, определяемый объектом myRange. Остается еще заметить, что если в левой части операторов присваивания А1 и А2 - это ссылки относительно объекта myRange, то в формулах правой части А1 и А2 привязаны к абсолютным адресам.
Наш следующий пример демонстрирует работу с объектом Selection:
Все сказанное по поводу предыдущего примера имеет место и в данном случае, когда свойство Range вызывается объектом Selection.
Рассмотрим теперь вызов Range с двумя параметрами:
'Example 5 Dim myRange1 As Range Set myRange1 = Range("E1", "E6") Debug.Print myRange1.Count myRange1.Range("A1") = 27 myRange1.Range("A2") = "=D1+2" myRange1.Range("A3:A6") = "=D1+D2"
Здесь в роли параметров метода Range выступают ячейки, первая из них определяет левый верхний, а вторая - правый нижний элемент диапазона объекта Range. Далее с этим объектом работаем также как и в предыдущем примере. В качестве параметров Cell1 и Cell2 могут выступать не только ячейки, но и объекты Range, что и демонстрирует следующий пример. Возвращаемый объект Range в этом случае представляет объединение областей, охватывающее область первого и второго объектов. Вот пример:
'Example 6 Dim myr1 As Range, myr2 As Range, myr3 As Range Set myr1 = Range("A11:C15") myr1 = 33 Set myr2 = Range("A13:F14") myr2 = 44 Set myr3 = Range(myr1, myr2) Debug.Print myr3.Cells.Count Debug.Print myr3.Cells(1, 1)
Прямоугольная область, полученная в результате, будет состоять из 30 элементов, и первый элемент этой области будет иметь значение 33, что и отражают результаты отладочной печати. Пример демонстрирует, как можно построить объединение диапазонов, дающее в результате новый непрерывный диапазон, включающий области обоих объектов. Покажем теперь, как построить "настоящее" объединение и пересечение диапазонов. В настоящем объединении могут быть несмежные диапазоны. Оно включает те и только те ячейки, которые принадлежат объединяемым объектам. Чтобы задать объединение или пересечение диапазонов, нужно вызвать Range с одним параметром, Этот параметр в этом случае представляет список элементов, разделенный знаком объединения -"," (запятая) или знаком пересечения - " " (пробел). Каждый элемент списка представляет диапазон, возможно, ячейку. Вот соответствующий пример
'Example 7 Dim myRange3 As Range 'Пример пересечения Set myRange3 = Range("A6:E6 E1:E6") ' Пересечение состоит из одного элемента E6 Debug.Print myRange3.Count myRange3.Select 'Пример объединения Dim myRange4 As Range Set myRange4 = Range("A6:E6, E1:E6") 'Объединение содержит 11 (!) элементов Debug.Print myRange4.Count myRange4.Select
Следует обратить внимание на несколько моментов. Во-первых, выделяется хотя и связная, но не прямоугольная область - такой уголок, состоящий из строки и столбца. Во- вторых, наше объединение не совсем "настоящее". В математике общие элементы присутствуют в одном экземпляре. Здесь же объединяются списки элементов без всякого их выбрасывания, так что "угол" E6 будет присутствовать дважды. В третьих, заметьте, при построении пересечения и объединения нельзя использовать переменные - диапазоны должны быть заданы константами. Следующий пример приводит к ошибке, если, конечно, убрать знаки комментария.
'Example 8 Dim myRange5 As Range 'Set myRange5 = Range("myRange1, myRange2")
Наконец, покажем, что в одном Range можно строить сколь угодно сложное объединение и пересечение элементов. Приведем для экзотики такой пример:
'Example 9 Dim myRange5 As Range Set myRange5 = Range("A6:E6, E1:E6, C1:C6 B5:D5") Debug.Print myRange5.Count myRange5.Select
Чтобы убедиться, что все построено правильно взгляните на картинку:
Рис. 3.20. Экзотический объект Range
Общие объекты и Excel.Application
Давайте начнем рассмотрение со свойств объекта Excel.Application , возвращающих уже знакомые нам общие объекты:
Таблица 3.1. Общие объекты, доступные в Excel.Application
Помощник, позволяющий организовать собственную диалоговую систему.
Office
Answer Wizard
Мастер Ответов, стоящий за спиной Помощника. Может использоваться при создании собственной справочной системы.
Office
Com AddIns
Коллекция компонент, общих для приложений Office 2000.
Office
CommandBars
Коллекция инструментальных панелей, без работы с которой не обойтись при создании собственного интерфейса документа Excel.
Office
FileSearch
Объект, используемый при поиске файлов.
Office
Language Settings
Объект, задающий языковые предпочтения, общие для приложений Office 2000.
Office
Debug
Объект, используемый при отладке программных проектов.
VBA
VBE
Корневой объект при работе с программными проектами.
VBA
Все объекты, приведенные в этой таблице, играют важную роль при программной работе с документами Excel, как, впрочем, и с другими документами Office 2000.
Построение обработчиков событий
Обработчики событий для объектов Workbook, Worksheet и объектов Chart, задающих листы диаграмм, построить нетрудно. Все эти события по умолчанию включены, поэтому для построения обработчика достаточно перейти в окно проектов, выбрать модуль, обрабатывающий события этого объекта, в окне объектов этого модуля выбрать нужный объект, а затем в окне событий и процедур выбрать из списка имя события. В результате этих действий появится заготовка, содержащая заголовок обработчика события, после чего останется написать код обработчика. Более сложно строятся обработчики событий для объектов Chart, задающих встроенные диаграммы. О том как создаются обработчики событий для тех объектов, у которых события по умолчанию выключены, я подробно рассказал при рассмотрении событий объекта Application. Остается только коротко напомнить схему действий:
Вначале нужно создать класс, в котором следует объявить объект Chart с событиями (With Events)
Затем нужно объявить глобальный объект созданного класса - объект, уже имеющий события. После этого остается написать инициализирующую процедуру, в которой связывается глобальный объект с объектом, задающим встроенную диаграмму.
Поскольку объект Chart с событиями появляется в окне созданного класса, то к нему применяется обычная схема создания обработчиков событий.
После запуска инициализирующей процедуры, объект, задающий встроенную диаграмму, будет реагировать на события.
Возможно, следует обратиться к началу главы, где все подробно описано на примере работы с объектом Application.
Пример обработки события Change
В качестве примера рассмотрим построение обработчиков события Change. Я рассмотрю обработку этого события на двух уровнях - уровне объекта Application, на нижнем уровне - объектом WorkSheet. Рассмотрение этого события представляет практический интерес, поскольку довольно часто в различных задачах приходится следить за изменениями, происходящими с рабочими страницами. С другой стороны, есть несколько важных нюансов, которые следует учитывать в процессе работы с этим событием. В моем примере обработчик события Change объекта Application будет следить за всеми изменениями, которые пользователь выполняет на рабочих страницах различных документов Excel. Информация об изменениях будет регистрироваться в журнале изменений, заданным специально спроектированной формой. Как выглядит сама форма, увидим чуть позже, а сейчас замечу, что устроена она очень просто и содержит один список из пяти столбцов, в каждом из которых будут храниться данные о документе, в котором произошли изменения, странице, дате, адресе области изменения и новом значении, записанном в эту область. Вот текст обработчика события, возникающего при инициализации формы: Private Sub UserForm_Initialize() 'Задание заголовков столбцов журнала изменений. With Me.ListBox1 .ColumnCount = 5 .AddItem "Книга" .Column(1, .ListIndex + 1) = "Страница" .Column(2, .ListIndex + 1) = "Дата" .Column(3, .ListIndex + 1) = "Адрес" .Column(4, .ListIndex + 1) = "Значение" End With End Sub
Приведу теперь текст обработчика события Change для объекта Application: Private Sub ExApp_SheetChange(ByVal Sh As Object, ByVal Target As Range) 'Запись в журнал всех изменений, проводимых пользователем. Dim RowIndex As Integer MsgBox ("Запись в журнал изменений!") RowIndex = JournalForm.ListBox1.ListCount With JournalForm.ListBox1 .AddItem Sh.Parent.Name .Column(1, RowIndex) = Sh.Name .Column(2, RowIndex) = Now .Column(3, RowIndex) = Target.Address .Column(4, RowIndex) = Target.Cells(1).Value End With End Sub
При каждом изменении на страницах любой из открытых рабочих книг, с которыми работает пользователь, соответствующая запись будет добавлена в список формы JournalForm. Заметьте, параметры Sh и Target, переданные обработчику события Change, позволяют однозначно задать всю информацию, требуемую для журнала. Взгляните, как выглядит наш журнал в процессе работы с ним:
Рис. 3.4. Журнал изменений, созданный обработчиком события Change В журнале нашли отражение почти все изменения, происшедшие со страницами рабочих книг - с разными ячейками, разными страницами, разными рабочими книгами. Почему не все изменения были зафиксированы в журнале, я скажу чуть позже, специально остановившись более подробно на объяснении этой ситуации. А сейчас давайте рассмотрим специальный обработчик этого же события, предусмотренный для страницы с именем "Лист1" книги BookThree. Вот его текст: Private Sub Worksheet_Change(ByVal Target As Range) Static NumChange As Integer Dim Myr As Range NumChange = NumChange + 1 MsgBox ("Пишу Изменения!") Set Myr = ThisWorkbook.Worksheets("Лист2").Range("A1") Myr.Offset(NumChange, 0) = Target End Sub
Изменения, происходящие с ячейками этой страницы, будут фиксироваться не только в общем журнале изменений, но и заноситься на следующий лист этой же книги. Этот лист, по существу, является журналом изменений данной конкретной страницы. Такова общая схема работы с событием Change. А теперь давайте поговорим о нюансах.
Смещение и свойство Offset
Мы только что сказали, что при создании объектов Range нельзя пользоваться смещением - доступен только формат А1. Тем не менее, можно использовать смещение, чтобы переходить от одного объекта Range к другому, например от одной ячейки к другой, отстоящей от первой на определенном расстоянии. Достигается это благодаря свойству Offset объекта Range. Это свойство, или если хотите метод, имеет два параметра: RowOffset и ColumnOffset - смещение по строкам и столбцам, и возвращает новый объект Range, отстоящий от прежнего на заданное расстояние. Вот пример создания нового объекта, смещенного относительно исходного: 'Example 12 Set myRange = Range("A1:A4") Set myRange1 = myRange.Offset(2, 3) myRange1.Select
Приведем еще один пример, когда смещение используется при работе с ячейками. Заодно продемонстрируем ряд полезных функций, позволяющих проанализировать тип значения, хранящегося в ячейках таблицы: 'Example 13 Dim currcell As Range For Each currcell In Range("E1:E6").Cells If Application.WorksheetFunction.IsText(currcell.Value) Then currcell.Offset(0, 1).Formula = "Text" ElseIf Application.WorksheetFunction.IsNumber(currcell.Value) Then currcell.Offset(0, 1).Formula = "Number" ElseIf Application.WorksheetFunction.IsLogical(currcell.Value) Then currcell.Offset(0, 1).Formula = "Logical" ElseIf Application.WorksheetFunction.IsError(currcell.Value) Then currcell.Offset(0, 1).Formula = "Error" ElseIf currcell.Formula = "" Then currcell.Offset(0, 1).Formula = "Пусто" End If Next currcell
Взгляните, как выглядят значения, хранящиеся в ячейках, и результаты их анализа:
Рис. 3.21. Результаты анализа значений, хранимых в ячейках E1- E6
События объекта Chart
В отличие от объекта Worksheet, все события которого могут быть обработаны на верхнем уровне, объект Chart имеет специфические события, сообщения о которых направляются только ему одному. Встроенные диаграммы и листы диаграмм, имеют одни и те же события. Разница состоит в том, что события встроенных диаграмм по умолчанию выключены, поэтому необходимо потрудиться, чтобы стало возможным их подключение и написание обработчиков событий. Рассмотрим список событий, связанных с объектом Chart:
Таблица 3.9. События объекта Chart
СобытиеПоявляется, когда пользователь или программа
Activate
Активизировал лист диаграмм. Естественно, его нет у встроенных диаграмм.
BeforeDoubleClick
Дважды щелкает кнопкой мыши на диаграмме
BeforeRightClick
Щелкает правой кнопкой мыши на диаграмме
Calculate
Добавил или изменил данные на диаграмме.
Deactivate
Активизировал новый лист и тем самым деактивировал старый.
DragOver
Перетащил данные, расположив их поверх диаграммы.
DragPlot
Перетащил диапазон ячеек, расположив их поверх диаграммы.
MouseDown
Нажал кнопку мыши при позиционировании ее над диаграммой.
MouseMove
Передвигает указатель мыши по диаграмме.
MouseUp
Закончил перемещение мыши и освободил кнопку.
Resize
Изменил размер диаграммы.
Select
Выделил некоторый элемент диаграммы
SeriesChange
Изменил значение точки ряда данных.
События объекта Excel.Application
Объект Excel.Application может обрабатывать 21 событие, возникающие при работе с теми или иными объектами приложения Excel. Почти половина из этих событий возникает в процессе работы с объектом Workbook, другая половина событий связана с такими объектами, как страница документа (объект Sheet) и окно (объект Window). Практически все события, за исключением одного события NewWorkBook, могут быть обработаны на двух уровнях - объектом Application и объектом Workbook. Но прежде чем поговорить, о том, какие события может обрабатывать объект Application, в каких случаях следует проводить обработку события на уровне объекта Application, а в каких - на уровне объекта Workbook, давайте разберемся, как заставить объект Excel.Application вообще реагировать на события.
События объекта Workbook
Со всеми событиями, которые может обрабатывать объект Workbook, мы уже знакомы. Всего таких событий 20, из них 9 событий связаны непосредственно с самим объектом Workbook, 8 - возникают на страницах рабочей книги и связаны также с объектом Sheet, три события связаны с объектом Window. Я напомню, что при возникновении события сообщение о нем операционная система посылает, как правило, нескольким объектам. Все они, каждый по-своему, могут обрабатывать это событие. Подробно обо всем этом рассказано при рассмотрении событий объекта Application.
События объекта Worksheet
Со всеми событиями, которые может обрабатывать объект Worksheet, мы уже знакомы. Всего таких событий 8. Я напомню, что при возникновении события сообщение о нем операционная система посылает, как правило, нескольким объектам. Поэтому, когда возникает событие, связанное с рабочим листом, сообщение о нем будет послано и объектам Workbook и Application, стоящим на верхних уровнях иерархии. Все они, каждый по-своему, могут обрабатывать это событие. Подробно обо всем этом рассказано при рассмотрении событий объекта Application. Замечу еще, что объект Worksheet это последний объект в иерархии, для которого определены события, на нижних уровнях иерархии таких объектов нет.
События, связанные с объектом Sheet
Ряд событий, которые возникают в процессе работы со страницами той или иной рабочей книги, также могут быть обработаны на уровне объекта Application. Естественно, что эти события могут быть обработаны и на более низких уровнях - как на уровне объекта Workbook, так и на следующем уровне объектами, задающими саму страницу - Worksheet и Chart. Понятно, что на самом верхнем уровне задается обработка, общая для всех страниц всех книг, на следующем уровне обработка, общая для страниц конкретной книги, и на нижнем уровне - обработка, специфическая для данной страницы. В таблице 3.4 дана сводка событий, связанных с объектом Sheet, обрабатываемых на уровне объекта Application.
Таблица 3.4. События, возникающие при работе с объектом Sheet
СобытиеКогда возникаетПараметры события
SheetActivate(Sh As Object)
Страница становится активной.
Вновь активированная страница передается обработчику события в качестве параметра.
SheetBeforeDoubleClick(Sh As Object, Target As Range, Cancel As Boolean)
При двойном щелчке левой клавиши мыши на рабочей странице, но до того, как выполнится макрос, задающий реакцию на щелчок. Событие не возникает на Chart-страницах.
Первый параметр передает обработчику события объект Sh, задающий рабочую страницу, на которой был произведен двойной щелчок. Второй параметр Target возвращает ячейку (объект Range), ближайшую к указателю мыши в тот момент, когда был произведен щелчок. Если в обработчике события изменить значение параметра Cancel на True, то отменится выполнение макроса, задающего реакцию на двойной щелчок.
SheetBeforeRightClick(Sh As Object, Target As Range, Cancel As Boolean)
Аналогично двойному щелчку, но при нажатии правой клавиши мыши.
Параметры сохраняют смысл, описанный для обработчика событий двойного щелчка.
SheetCalculate(Sh As Object)
При перевычислениях рабочей страницы или при любых изменениях данных, отображаемых на диаграмме страницы диаграмм.
В зависимости от того, на странице какого типа произошло событие, параметр Sh представляет либо объект Workbook либо объект Chart.
SheetChange(Sh As Object, Target As Range)
При изменениях в ячейках рабочей книги, инициированные пользователем или внешней ссылкой. Не возникает для Chart-страниц.
Параметр Sh задает объект WorkSheet - страницу, в ячейках которой произошли изменения. Параметр Target задает область изменения - объект Range.
SheetDeactivate(Sh As Object)
Страница перестает быть активной, поскольку активной становится другая страница.
Деактивированная страница передается обработчику события в качестве параметра.
SheetFollowHyperlink(Sh As Object, Target As Hyperlink)
При щелчке по гиперссылке на рабочей странице. Не возникает для Chart-страниц.
В качестве параметров обработчику события передаются два объекта, представляющие рабочую страницу и гиперссылку, задающую переход.
SheetSelectionChange(Sh As Object, Target As Range)
При изменении области выделения рабочей страницы. Не возникает для Chart-страниц.
Параметр Sh задает объект WorkSheet -страницу, содержащую новую область выделения. Параметр Target задает новую область выделения - объект Range.
События, связанные с объектом Window
Объект Application может обработать три события, возникающие в процессе работы с окном - объектом Window. Эти события показаны в следующей таблице.
Таблица 3.5. События, возникающие при работе с объектом Window
СобытиеКогда возникаетПараметры события
WindowActivate(Wb As Workbook, Wn As Window)
Окно рабочей книги становится активным.
Рабочая книга и вновь активированное окно передаются обработчику события в качестве параметров.
WindowDeactivate(Wb As Workbook, Wn As Window)
Окно рабочей книги перестает быть активным.
Рабочая книга и деактивированное окно передаются обработчику события в качестве параметров.
WindowResize(Wb As Workbook, Wn As Window)
Окно рабочей книги изменяет размеры.
Рабочая книга и перестраиваемое окно передаются обработчику события.
Полагаю, что в дополнительных комментариях и примерах эти события не нуждаются.
События, связанные с рабочей книгой
В нижеследующей таблице 1 дана сводка всех событий, которые возникают при работе с рабочими книгами - объектами Workbook, и которые могут быть обработаны объектом Application.
Таблица 3.3. События, возникающие при работе с объектом Workbook
СобытиеКогда возникаетПараметры события
NewWorkbook(Wb As Workbook)
При создании новой книги. Единственное событие этой группы, которое может обработать только объект Application.
Обработчику события передается объект Wb, представляющий вновь созданную книгу.
WorkbookActivate(Wb As Workbook)
Книга становится активной.
Вновь активированная книга передается обработчику события в качестве параметра.
WorkbookAddinInstall(Wb As Workbook)
При установке рабочей книги в качестве AddIn.
Рабочая книга, представляющая AddIn.
WorkbookAddinUninstall(Wb As Workbook)
Отменяется установка рабочей книги в качестве AddIn. Закрытие книги при этом не происходит.
Рабочая книга, представляющая AddIn.
WorkbookBeforeClose(Wb As Workbook, Cancel As Boolean)
При попытке закрыть рабочую книгу, но до того, как она будет закрыта.
Параметр Wb задает закрываемую книгу. Параметр Cancel позволяет отменить закрытие, если в обработчике события его значение будет установлено как True.
WorkbookBeforePrint(Wb As Workbook, Cancel As Boolean)
При попытке распечатать содержимое рабочей книги, но до того, как произойдет печать.
Параметр Wb задает печатаемую книгу. Параметр Cancel позволяет отменить печать, если в обработчике события его значение будет установлено как True.
WorkbookBeforeSave(Wb As Workbook, SaveAsUI As Boolean, Cancel As Boolean)
При попытке сохранить содержимое рабочей книги, но до того, как произойдет сохранение.
Параметр Wb задает сохраняемую книгу. Параметр Cancel позволяет отменить сохранение, если в обработчике события его значение будет установлено как True. Параметр SaveAsUI показывает, как идет сохранение, его значение равно true, если при сохранении открывается диалоговое окно "Сохранить как ".
WorkbookDeactivate(Wb As Workbook)
Книга перестает быть активной, поскольку активной становится другая книга.
Деактивированная книга передается обработчику события в качестве параметра.
WorkbookNewSheet(Wb As Workbook, Sh As Object)
При добавлении новой страницы в рабочую книгу.
Объект Wb задает книгу, а Sh - страницу, добавленную в эту книгу.
WorkbookOpen(Wb As Workbook)
При открытии уже существующей рабочей книги.
Обработчику события передается объект Wb, представляющий вновь открытую книгу.
Практически все события, происходящие с объектом Workbook, могут быть обработаны на двух уровнях - объектом Application и самим объектом Workbook. Разница лишь состоит в том, что если у объекта Application есть, например, событие WorkbookOpen, то у объекта Workbook есть событие Open. При возникновении данного события операционная система посылает соответствующее сообщение двум объектам - Application и Workbook. Обработчику сообщения WorkbookOpen передается параметр Wb, задающий открываемую книгу. Понятно, что при посылке аналогичного сообщения объекту Workbook передавать этот параметр не имеет смысла, поскольку он и так знает сам себя, так что обработчик события Open объекта Workbook параметров не имеет. Возникает естественный вопрос, в каких случаях следует вести обработку события, происходящего с объектом Workbook, на уровне объекта Application. Ответ понятен - в тех случаях, когда обработчик события выполняет действия, общие для всех рабочих книг. В тех же случаях, когда предполагается специфическая обработка события, характерная только для данной конкретной книги, обработчик события связывается с объектом Workbook. Что происходит, если обработка одного и того же события предусмотрена на двух уровнях? В этом случае вначале выполнится обработчик события, связанный с объектом Workbook, - он выполнит специфическую для данной книги обработку, а потом начнет работать обработчик этого же события, связанный с объектом Application, выполняющий ту часть работы, которая является общей для всех рабочих книг. Приведу пример, в котором предусмотрена общая для всех рабочих книг обработка события BeforeSave: Private Sub ExApp_WorkbookBeforeSave(ByVal Wb As Workbook, _ ByVal SaveAsUI As Boolean, Cancel As Boolean) Dim YesNo As Variant YesNo = MsgBox("Вы действительно хотите сохранить этот документ?", vbYesNo) If YesNo = vbNo Then Cancel = True End Sub
Предупреждающее сообщение будет появляться для всех рабочих книг. Рассмотрим теперь пример, когда обработка события BeforePrint предусмотрена на двух уровнях: Private Sub Workbook_BeforePrint(Cancel As Boolean) 'Обработка события - печать содержимого книги. MsgBox ("Эту книгу - " & ThisWorkbook.Name _ & " печатать запрещено!") Cancel = True End Sub
Private Sub ExApp_WorkbookBeforePrint( ByVal Wb As Workbook, Cancel As Boolean) 'Обработка события - печать содержимого книги. If Wb.ActiveSheet.Name = "Лист1" Then MsgBox ("Эту страницу книги - " & Wb.Name _ & " печатать запрещено!") Cancel = True End If End Sub
Специальный обработчик события объекта Workbook книги BookOne запрещает печать только этой книги, а общий для всех книг обработчик того же события, но находящийся в объекте Application, запрещает печать только первого листа, но для всех рабочих книг. Вот как выглядит сообщение, выданное для книги BookOne общим обработчиком при попытке распечатать содержимое первого листа книги:
увеличить изображение Рис. 3.3. Сообщение, выданное обработчиком события WorkbookBeforePrint
Создание объекта Application, реагирующего на события
Для всех приложений Office 2000 соответствующие объекты Application хотя и могут реагировать на события, но появляются как объекты без событий. И нужно приложить некоторые усилия, чтобы создать новый объект Application With Events, который может реагировать на события. Причина возникающих сложностей кроется в логике построения программного проекта, изначально принятой в Office. Дело в том, что обработчики событий, возникающих при работе с теми или иными объектами, следует размещать в специальных модулях - обработчиках событий. Для большинства стандартных объектов Office эти модули создаются автоматически в момент создания основного объекта. Так, например, при создании документа в его проекте автоматически создается модуль, обрабатывающий события этого документа, - для Excel это модуль с именем "ЭтаКнига" (ThisWorkbook). Только в этом модуле и можно создать обработчики событий объекта Workbook и всех объектов, реагирующих на события, например, элементов управления, встраиваемых непосредственно в документ. Объект Application, единый для всех рабочих книг, по понятным причинам не вписывается в эту общую схему и для него такой модуль автоматически не создается, потому, естественно, эту работу приходится выполнять программисту. Для того чтобы заставить реагировать на события объект Excel.Application, необходимо выполнить четыре шага:
Создать модуль, задающий собственный класс для объекта Excel.Application, в котором можно будет размещать обработчики возникающих событий. Устроен этот класс очень просто и состоит в момент создания из одной строчки, поскольку класс не имеет методов и имеет ровно одно свойство, задающее объект Application с событиями. Конечно, позже этот класс будет пополняться обработчиками событий. Вот как выглядит на этапе создания описание этого класса, которому я в своем примере дал имя AppWithEvents. Option Explicit 'Класс, описывающий объект Application With Events. 'Класс не имеет методов, задается одним свойством. Public WithEvents ExApp As Application
Следующее, что необходимо сделать - это определить объект вновь созданного класса AppWithEvents. И эта задача решается в одну строчку. Объявление объекта я поместил в раздел объявлений созданного ранее стандартного модуля. Заметьте, необходимо не только объявить объект, но и создать его, используя спецификатор New. Public AppWithEv As New AppWithEvents
Третьим шагом является связывание двух объектов Application, стандартного и созданного объекта, умеющего реагировать на события. По-видимому, лучшим местом, где следует осуществить связывание, является обработчик события Open того документа, в котором описан класс AppWithEvents. Тогда после открытия этого документа и вплоть до его закрытия Excel будет реагировать на события, связанные с объектом Excel.Application. Вот как осуществляется связывание: Private Sub Workbook_Open() 'Связывание двух объектов Application - 'стандартного и реагирующего на события. Set AppWithEv.ExApp = Excel.Application End Sub
На этом шаге созданный класс AppWithEvents можно расширить, добавив в него обработчики событий объекта Application. Я приведу пока текст только одного обработчика, который обрабатывает событие NewWorkBook, возникающее при создании новой книги. Private Sub ExApp_NewWorkbook(ByVal Wb As Workbook) 'Обработка события - создание новой книги. Static Num As Integer Num = Num + 1 MsgBox ("Число вновь созданных книг - " & Num _ & vbCrLf & "Новая книга - " & Wb.Name _ & " открыта в " & Time) End Sub
Вот как выглядит сообщение, появляющееся при открытии новой книги:
Рис. 3.2. Сообщение об открытии новой книги
Сравнение свойств объектов Range и Worksheet
У этих двух объектов есть целый ряд общих свойств. Вот они:
Свойства, возвращающие объект Range: Range, Cells, Columns, Rows. Понятно, что с помощью, например, свойства Range можно выделить некоторую область не только из области заданной рабочим листом, но и из любой подобласти, определенной объектом Range. Это же относится и ко всем другим свойствам, возвращающим объект Range, напоминающий матрешку.
Hyperlinks - возвращает коллекцию гиперссылок, принадлежащих области объекта Range.
Целый ряд свойств объекта Range возвращают единственный объект, в то время как родительский объект Worksheet возвращает всю коллекцию. Вот эти свойства:
Name - для Range возвращается не строка, задающая имя, а объект Name.
Comment - комментарий.
PivotTable - сводная таблица.
QueryTable - таблица запросов.
Обратите внимание, на объект Range, возвращающий единственный объект, накладываются определенные требования. Так для того, чтобы вернуть комментарий, необходимо, чтобы объект Range представлял единственную ячейку, содержащую комментарий. Сводная таблица должна содержать верхний левый угол объекта Range. Честно скажу, логика создателей объектной модели не очень понятна. Почему возвращается коллекция гиперссылок, но не возвращается коллекция комментариев или сводных таблиц, которых, вообще говоря, может быть несколько в области объекта Range. На такие вопросы ответов нет, нужно просто знать спецификации. Вот небольшой пример работы со свойством Comment: 'Example 13 - какой комментарий возвращается? Dim Sh As Worksheet Dim myr As Range, s As String, c As Comment Set Sh = ThisWorkbook.Worksheets(1) Set c = Sh.Comments(1) s = c.Text Debug.Print s Set myr = Sh.Range("C16") myr.Select Set c = myr.Comment s = c.Text Debug.Print s
Следующую группу составляют похожие свойства. Я отношу к ним следующие свойства:
OutlineLevel, - свойство, определенное для объектов Range, представляющих строки или столбцы. Задает уровень структурирования для текущей строки или столбца и связано со свойством Outline объекта Worksheet.
PageBreak - разрывы страниц, также устанавливаемые для строк и столбцов. Свойство связано со свойствами HPageBreaks и VPageBreaks.
Упомяну еще ряд свойств, так или иначе пересекающихся со свойствами родительского объекта:
CurrentArray, CurrentRegion - Первое из этих свойств возвращает весь массив, частью которого является объект Range (ячейка). Второе - возвращает текущий регион, то есть минимальную прямоугольную область, содержащую элементы из Range и окаймленную пустыми строками и столбцами или границами таблицы. Если Range представляет связную область, то текущий регион охватывает Range, для несвязной области он выделяет лишь некоторую его часть.
EntireColumn, EntireRow - Эти два свойства возвращают один или несколько столбцов или строк, охватывающих объект Range.
End - используется для объекта Range, представляющего одну ячейку. В качестве результата возвращается объект Range, также представляющий одну ячейку в конце региона, содержащего вызывающий свойство объект Range. Вот как, например, можно, используя это свойство, выделить область от искомой ячейки до конца региона:
'Example 14 Set myr = Sh.Range("F12") Sh.Range(myr, myr.End(xlToRight)).Select
Dependents, Precedents, DirectDependents, DirectPrecedents - В первых двух случаях возвращается объект Range, содержащий все ячейки, зависимые или предшествующие ячейкам исходного объекта. Результат, обычно, представляет несвязную область. В двух последних случаях возвращаются только непосредственно зависимые и непосредственно предшествующие ячейки. Возможно, стоит определить понятия зависимых и предшествующих ячеек. Если формула в ячейке Y содержит ссылку на ячейку X, то говорят, что Y непосредственно зависит от X, а X непосредственно предшествует Y. Обобщая понятие непосредственной зависимости, мы говорим, Y зависит от X, а X предшествует Y, если существует цепочка ячеек Z1, Z2, …ZK, начинающаяся с X и заканчивающаяся Y, такая, что каждые два соседние элемента цепочки связаны отношением непосредственной зависимости (непосредственного предшествования). Свойство CircularReferences объекта Worksheet, возвращающее ячейки, связанные циклической зависимостью, тоже относится к этой группе свойств. О зависи мых ячейках и примерах использования этих свойств подробный разговор еще предстоит.
PivotItem, PivotField - возвращают элементы сводной таблицы, хранящиеся в объекте Range.
Свойства и методы объекта Range
Об объекте Range можно говорить сколь угодно долго - это основа Excel. У него есть большое число свойств и методов, но нет событий, поскольку события связаны с объектами, стоящими не более высоких уровнях иерархии. Заметьте, со многими свойствами объекта Range мы уже знакомы. В этом нет ничего удивительного, поскольку Range задает часть рабочего листа, а свойства части и целого во многом совпадают. Поэтому давайте начнем изучение свойств объекта Range в сравнении с уже знакомыми свойствами объекта Worksheet.
Свойства объекта Worksheet
Среди свойств, как всегда, наибольший интерес представляют свойства-участники, возвращающие некоторый отдельный объект или коллекцию в качестве результата. Эти свойства определяют структуру объекта Worksheet, задавая непосредственно вложенные в него объекты.
Свойства-участники объекта Workbook
Свойств, возвращающих объекты, у объекта Workbook относительно немного. Поэтому коротко можно рассказать о каждом. При их описании я разделю их на группы:
Группа свойств, возвращающих основные объекты рабочей книги. К ним я отношу следующие свойства:
Sheets, Charts, WorkSheets, Excel4MacroSheets - все они возвращают объект класса Sheets, но в зависимости от свойства этот объект представляет либо всю коллекцию листов рабочей книги, либо ту ее часть, которая содержит листы типа, заданного именем свойства
ActiveSheet - возвращает объект класса Object, содержащий активный лист рабочей книги. Тип активного листа и определяет тип возвращаемого объекта.
ActiveChart - возвращает объект класса Chart - это может быть лист-диаграмма, если активный в момент вызова лист принадлежит этому типу, либо диаграмма, встроенная в активный рабочий лист, если и сама диаграмма на этом листе является в свою очередь активной. В противном случае при вызове свойства будет возвращен результат Nothing, свидетельствующий о том, что объект не найден.
Группа свойств, возвращающих общие или схожие объекты. Это самая большая группа свойств объекта Workbook, что говорит о том, что общих черт у объекта Workbook больше чем специфических.
VBProject - общий объект, описывающий программный проект приложений Office 2000. Необходим при программировании "на лету" - программной работе на VBA с программным проектом.
HMLProject - общий объект библиотеки Office, описывающий проект в формате HTML. Этот объект используется при программировании на VBScript.
WebOptions - объект, схожий во всех приложениях Office 2000, позволяющий задавать параметры Web-страниц. Действие большинства параметров распространяются на все приложения Office 2000.
Windows - коллекция окон, связанных с рабочей книгой. Особенностью документа Excel от документов других типов состоит в том, что документ Excel можно одновременно открыть в нескольких окнах, чтобы в каждом из них видеть требуемую часть рабочей книги. Сами же объекты Window схожи во всех приложениях. Эта коллекция является частью более общей коллекции окон, возвращаемой аналогичным свойством объекта Application.
RoutingSlip - объект, схожий для всех приложений Office 2000. Он определяет характеристики передачи рабочей книги по сети участникам, совместно работающим над данным документом. При работе с ним должно быть включено булево терминальное свойство HasRoutingSlip.
Styles - объект, схожий для приложений Office 2000 - определяет коллекцию применяемых для форматирования стилей. Есть набор встроенных стилей, но в коллекцию можно добавлять и собственные стили, задав в момент добавления соответствующие характеристики стиля - шрифт, размер, рамки и т.д. Обычно стили создаются руками, но, возможно и программное создание или изменение того или иного стиля.
BuiltinDocumentProperties, CustomDocumentProperties - оба эти свойства возвращают объект класса DocumentProperties из общей библиотеки Office. Этот класс определяет коллекцию свойств документов Office 2000. Эта коллекция напоминает коллекцию Sheets тем, что ее элементы могут относиться к различным типам. Свойство BuiltinDocumentProperties возвращает ту часть коллекции, которая задает стандартные свойства, заданные самим приложением, CustomDocumentProperties - свойства, определенные пользователем для конкретного документа. Свойства документов, являются схожими для всех приложений Office 2000. С двумя типами свойств работа идет по-разному. Коллекция стандартных свойств определена для каждого типа документа Office 2000, - добавлять или удалять элементы этой коллекции нельзя. В тоже время, коллекция пользовательских свойств изначально пуста и добавление или удаление свойств является основным способом работы с этой коллекцией. Большинство из стандартных свойств автоматически изменяют свои значения в процессе работы с документом, отражая, например, текущие характеристики документа - количество страниц, символов, дату последнего изменения и другие характеристики. Значения других стандартных свойств, также как и всех пользовательских свойств могут быть заданы пользователем или программно.
Mailer - схожий для всех приложений Office 2000 почтовый объект, используемый при работе на компьютерах Макинтош.
К третьей группе относятся свойства, возвращающие объекты, специфические для рабочих книг Excel. К ним относятся:
Names - коллекция имен, которые даны отдельным областям листов рабочей книги. Типичной является ситуация, когда при работе с рабочей страницей, выделяется та или иная область, содержащая определенные данные, и этой области даются имена. При программировании очень часто приходится использовать имена таких объектов. Полагаю, что примеров в свое время будет достаточно. Напомню, что объект Application также имеет свойство Names, возвращающее коллекцию имен, включающую коллекции имен всех открытых рабочих книг.
PublishObjects - возвращает одноименную коллекцию, элементы которой принадлежат классу PublishObject. Эти объекты являются новинкой Excel 2000, они задают элементы рабочей книги, опубликованные на Web-странице. С их помощью можно вести обновление Web-страницы при изменениях состояния опубликованных объектов.
Container - объект, который содержит рабочую книгу. Типичным примером, когда рабочая книга может быть встроена в другой объект, и являться его частью, служит подшивка - объект Binder. Следует заметить, что в отличие от предыдущей версии подшивки пока не стали частью семейства Office 2000.
CustomViews - возвращает одноименную коллекцию, элементы которой задают образ той или иной части рабочей книги в том виде, как он виден на экране дисплея. Каждому такому образу дается свое имя. Ранее я говорил, что для того, чтобы увидеть ту или иную часть рабочей книги, используются окна. Custom View - это другая возможность задать коллекцию образов экрана, отражающих заданные части рабочей книги. В нижеследующем примере первая из приведенных процедур создает объект CustomView, сохраняющий текущий образ рабочей книги. Вторая процедура, которую можно вызвать в подходящий момент, позволяет показать на экране нужную часть рабочей книги в соответствии с сохраненным образом.
Public Sub AddCustView() 'Добавить Custom View - образ экрана, 'отражающий текущий вид рабочей книги. ThisWorkbook.CustomViews.Add ("Две кнпки") Debug.Print ThisWorkbook.CustomViews.Count ThisWorkbook.CustomViews(1).Show End Sub
Public Sub ShowCustView() 'Показать Custom View - вывести на экран, 'заданный вид рабочей книги. Dim CW As CustomView If ThisWorkbook.CustomViews.Count > 0 Then For Each CW In ThisWorkbook.CustomViews If CW.Name = "Две кнопки" Then CW.Show Next CW End If End Sub
Свойства - участники объекта
Рассмотрим теперь свойства - участники объекта Excel.Application, возвращающие объекты, специфические для Excel, Как я и предупреждал, я рассмотрю лишь основные свойства, которые действительно необходимы при работе с объектом Excel.Application.
Таблица 3.2. Основные свойства - участники
Свойство, возвращающее объектНазначение объекта
WorkBooks
Коллекция открытых в Excel документов - рабочих книг. Основной объект, благодаря которому можно получить доступ к любому документу Excel и далее работать с объектами этой рабочей книги.
Windows
Коллекция открытых окон во всех рабочих книгах. Дело в том, что одну и ту же рабочую книгу часто полезно открывать в нескольких окнах, что позволяет видеть разные участки рабочей книги. Коллекция Windows позволяет получить доступ к каждому такому окну. Чаще всего, свойство Windows используется при работе с объектом WorkBook, для объекта Application это один из примеров той перегрузки, о которой я упоминал выше.
WorkSheetFunction
Объект - контейнер, в котором находятся многочисленные функции Excel, начиная от обычных математических функций и кончая функциями, применяемыми для решения задач статистики, прогноза, работы с датами и прочими.
AddIns
Коллекция компонент, расширяющих возможности решения специальных задач в Excel.
AutoCorrect
Знакомый по приложению Word объект, позволяющий задавать автоматическую корректировку набираемых текстов в ячейках Excel.
DefaultWebOptions
Объект, позволяющий устанавливать параметры для документов Excel, сохраненных в виде Web-страниц. Схож с аналогичным объектом Word.Application, но имеет свою специфику.
Dialogs
Объект Dialogs также как и три предыдущих объекта - AddIns, AutoCorrect, DefaultWebOptions относится к группе схожих объектов, встречающихся в каждом из приложений Office 2000, имеющих много общего, но имеющих и отличия, связанные со спецификой приложения. Также как и в Word, объект Dialogs задает коллекцию стандартных диалоговых окон, которые могут открываться в Excel, позволяя организовать диалог с пользователем.
Names
Одно из перегруженных свойств, возвращающее коллекцию всех имен, используемых для отдельных ячеек и областей всех открытых документов Excel. Чаще всего, это свойство используется при работе с отдельной рабочей книгой или отдельной страницей.
ODBCErrors
Коллекция объектов класса ODBCError. Элементы этой коллекции создаются автоматически источником ODBC-данных, если при выполнении запроса на получение данных возникли ошибки. Если ошибок не было, то и коллекция будет пустой.
OLEDBErrors
Коллекция объектов класса OLEDBError. Аналогично предыдущей коллекции, ее элементы появляются при наличии ошибок в процессе работы с базой данных, когда используется интерфейс OLE DB.
RecentFiles
Объект, относящийся к группе схожих объектов семейства Office 2000. Он задает коллекцию файлов, хранящих документы Excel последнего использования.
Конечно, непросто устроен и объект Series, задающий сам ряд данных. Его структура показана на рис. 3.19, а некоторые из основных его свойств были продемонстрированы в предыдущем примере.
DataTable- объект, представляющий таблицу данных. Основными методами являются Select и Delete, основные свойства связаны с рамкой, строящейся вокруг таблицы. Вся содержательная работа с данными таблицы ведется через другие объекты (ряды данных). Так что по существу этот объект представляет рамку таблицы данных.
Legend - задает легенду диаграммы. O сущности этого объекта мы уже говорили, так что его поведение должно быть понятно.
Shapes- эта коллекция нам хорошо знакома. В диаграммах она используется редко, но иногда можно категории изображать рисунками, что повышает эстетику диаграммы.
ChartGroups(ChartGroup) - возвращает коллекцию групп. Элементами коллекции являются объекты класса ChartGroup. Каждый такой объект представляет группу, связанную с одним или несколькими рядами данных, отображаемых диаграммой одного и того же типа, о чем я уже подробно рассказывал. Параметр Index позволяет добраться до конкретной группы в коллекции. Поскольку при форматировании одной из группы индексы изменяются, то иногда удобнее пользоваться специальными методами, которые возвращают группы фиксированного типа. Такими методами являются: AreaGroups, BarGroups, ColumnGroups, DoughnutGroups, LineGroups и PieGroups. Эти методы для двумерных диаграмм возвращают коллекцию групп типа, указанного методом. К конкретной группе можно добраться опять - таки с помощью индекса. Для трехмерных диаграмм может быть только одна группа определенного формата. Поэтому есть методы, возвращающие эту единственную группу: Area3DGroup, Bar3DGroup, Column3DGroup, Line3DGroup, Pie3DGroup, SurfaceGroup.
Floor, Walls и Corners объекты используются при работе с трехмерными диаграммами. При отображении таких диаграмм для создания иллюзии трехмерного пространства диаграмма отображается на фоне открытого куба, имеющего основание, заданное объектом Floor, и две боковые грани - объекты Walls. Объект Corners задает углы куба. Манипулируя этими объектами, можно, например, развернуть куб или изменить заливку и узор стенок куба, добиваясь большей наглядности изображения.
Свойства - участники
Дадим краткую характеристику свойствам - участникам, входящим в рабочий лист:
Range, Cells, Rows, Columns, UsedRange, CircularReference - начнем с группы свойств, возвращающих объект Range. Я уже говорил, что объект Range - это основной объект электронной таблицы. Он позволяет задать, как отдельную ячейку таблицу, диапазоны ячеек, представляющие прямоугольную область таблицы, так и области более сложной конфигурации. Именно объект Range со своими свойствами и методами позволяет осуществлять непосредственную работу, как с отдельной ячейкой, так и с областями ячеек. Об этом объекте я еще много буду говорить, но уже сейчас хочу заметить, что большинство свойств рабочего листа Worksheet, о которых пойдет речь, характерны и для объекта Range, задающего часть рабочего листа. Объект Range возвращается в качестве результата при вызове следующих свойств:
Range(Cell1, [Cell2]) As Range - возвращает объект Range, определяемый параметрами свойства. Синтаксис параметров таков, что он позволяет определить достаточно изощренный объект. Я расскажу об этом подробнее чуть позже, когда мы займемся подробным рассмотрением объекта Range.
Cells As Range - возвращает коллекцию ячеек электронной таблицы. Вызванное объектом WorkSheet это свойство возвращает всю таблицу ячеек рабочего листа, которая, конечно, представляет собой объект Range. Поскольку Cells одновременно является объектом Range и коллекцией ячеек, то можно использовать индексы, чтобы добраться до отдельного элемента коллекции - ячейки таблицы. Важным свойством Cells обладают и объекты, стоящие на более низких ступенях иерархии, в частности, им обладает и сам объект Range, что позволяет получить коллекцию ячеек для любой заданной области таблицы.
Rows As Range и Columns As Range - соответственно возвращают коллекции строк и столбцов таблицы. По индексу можно добраться до отдельной строки или столбцу таблицы. Одновременно эти коллекции являются объектами Range, поскольку задают некоторую область рабочего листа.
UsedRange As Range - возвращает используемую область рабочего листа. Как правило, лишь небольшая часть рабочего листа занята данными, формулами, рисунками, диаграммами и графиками. Свойство UsedRange позволяет получить минимальную прямоугольную область, содержащую используемую область рабочего листа.
CircularReference As Range - возвращает объект Range, содержащий первую циклическую ссылку, если таковые имеются на рабочем листе. В противном случае возвращается значение Nothing. О циклических ссылках поговорим подробнее чуть позже.
Одну и ту же область таблицы - один и тот же объект Range - можно получить разными способами. Приведем пример, показывающий два способа получения ячейки "А1": Debug.Print ActiveSheet.Range("A1") Debug.Print ActiveSheet.Cells(1, 1)
Оба оператора здесь эквивалентны, но только в одном случае используется свойство Range, в другом - Cells. В следующем примере работа идет над отдельным столбцом и строкой, но, фактически, и здесь действует тот же объект Range: ActiveSheet. Columns(2).Value = "Да" ActiveSheet.Rows(1).Font.Bold = True
Shapes - возвращает одноименную коллекцию, элементами которой являются объекты класса Shape. Эта коллекция состоит из объектов самых различных типов. По существу, все, что вставляется в рабочий лист, - рисунки, диаграммы, графики, встроенные и связанные OLE-объекты, элементы управления, размещаемые на рабочем листе, - все это объекты коллекции Shapes. При программировании нам неоднократно придется сталкиваться с этими объектами. После объектов Range объекты Shape представляют наиболее часто встречающийся тип объектов, определяющих суть рабочего листа. Замечу еще, что коллекция Shapes и объекты Shape относятся к схожим объектам, встречающимся во всех приложениях Office 2000. При описании объектов Word нам уже приходилось встречаться с этими объектами.
Names - возвращает неоднократно упоминавшуюся одноименную коллекцию. Подобное свойство имеют объекты Application и Workbook, имеет его и объект WorkSheet. Разница состоит только в том, что здесь речь идет об именах, используемых в данном рабочем листе.
Comments - возвращает одноименную коллекцию с элементами класса Comment, представляющими комментарии, которые можно привязывать к той или иной ячейке рабочего листа - объекту Range. Замечу, что комментарии несут очень важную нагрузку в создании дружелюбного интерфейса разрабатываемых документов. Их следует широко использовать. Эти объекты также относятся к схожим объектам, и встречаются во всех приложениях Office 2000. Но в Excel комментарии к ячейкам играют особую роль, значительно более важную, чем комментарии в документах Word. В текстовых документах можно использовать различные стили, чтобы непосредственно в тексте документа вставлять авторские замечания, предупреждения и пояснения. Комментарии, как правило, используются в тех случаях, когда не желательно прерывать плавное изложение материала, чтобы работающий с документом мог прочесть основной текст, лишь при желании знакомясь с комментариями. В Excel'е, когда основное содержание листа составляют таблицы, все замечания, пояснения и предупреждения оформляются в виде комментариев к тем или иным ячейкам. Они, например, могут давать подсказку, какие данные располагаются в ячейке, определять их формат и давать другую полезную информацию, необходимую пользователю при работе с таблицами рабочего листа. Есть некоторая разница в программной работе с комментариями в Word и Excel. Вот как выглядит пример введения комментариев в документ Excel: Public Sub AddComments() 'Формируется последовательность чисел Фибоначчи. 'Вставляется комментарий, поясняющий суть чисел. Dim myRange As Range Workbooks("BookThree").Activate With ActiveWorkbook.Worksheets(2) Set myRange = .Range("E1") With myRange .Value = "Числа Фибоначчи" .Offset(1, 0).FormulaR1C1 = "0" .Offset(2, 0).FormulaR1C1 = "1"
.Offset(3, 0).FormulaR1C1 = "=R[-2]C +R[-1]C" .Offset(3, 0).Select Selection.AutoFill Destination:=Range("E4:E20"), _ Type:=xlFillDefault End With 'Добавление комментария myRange.AddComment "Числа Фибоначчи - это ..." .Comments(1).Visible = False If (.Comments(1).Author = "Vladimir Billig") Then Debug.Print "OK!" End If 'Показ и удаление комментария .Comments(1).Visible = True '.Comments(1).Delete End With
End Sub
В этом примере я вначале программно формирую последовательность чисел Фибоначчи, а затем к ячейке, задающей заголовок, добавляю комментарий, поясняющий сущность этих чисел. Заметьте, в Excel в отличие от Word коллекция комментариев не имеет метода Add, - они вводятся специальным методом AddComment объекта Range. Они по-другому показываются, используя свойство Visible, что, пожалуй, более естественно. Заметьте также, что свойство Author можно использовать только для чтения.
QueryTables - возвращает одноименную коллекцию с элементами класса QueryTable, каждый из которых представляет таблицу, полученную на основе запроса к внешнему источнику данных. Внешним источником данных может быть база данных, Web-страницы в Интернет. Я уже вскользь упоминал о построении Web-запроса, когда рассматривал внешние ссылки при описании объекта Application. Следует сказать, что, несмотря на то, что объект QueryTable присутствовал и в предыдущей версии Office 97, в нынешней версии этот объект претерпел существенные изменения, и у него появилось множество новых свойств, что, в первую очередь, связано с возможностью построения Web-запросов. Об этом объекте я еще скажу более подробно чуть позже.
Hyperlinks - возвращает одноименную коллекцию с элементами класса Hyperlink, - гиперссылками, задающими связи (переходы) ячеек рабочего листа с внешним миром. В качестве гиперссылки может, например, использоваться URL-адрес в Internet. Объекты Hyperlink относятся к группе схожих объектов, встречающихся во всех приложениях Office 2000.
Outline - возвращает одноименный объект, задающий структурированное представление рабочего листа. Зачастую данные, представленные на рабочем листе можно структурировать, сжимая или разворачивая их по мере необходимости. Типичной является ситуация, когда данные, отражающие работу некоторого предприятия, представлены по дням, неделям, месяцам, кварталам и годам. При глобальном анализе деятельности предприятия нас могут интересовать только сводные результаты за каждый год, в этом случае нижние уровни структуры будут свернуты, но при необходимости их всегда можно развернуть вплоть до ежедневного анализа. Поскольку таблица двумерная, то возможны два направления свертки данных. Так, например, второе направление может отражать структуру предприятия: цеха, участки, группы, отдельного работника. Метод ShowLevels(RowLevels, ColumnLevels) объекта Outline позволяет показать структуру рабочего листа, где уровни детализации по строкам и столбцам задают параметры метода. Чаще всего, для проведения подобного анализа целесообразнее использовать сводные таблицы - объект PivotTablle.
AutoFiter - возвращает одноименный объект, позволяющий производить фильтрацию данных в специального рода Excel-запросах. О фильтрах и их использовании я расскажу подробно и приведу соответствующие примеры.
Свойства Next и Previous возвращают следующую и предыдущую страницу рабочей книги.
Коллекции HPageBreaks и VpageBreaks, возвращаемые при вызове одноименных свойств, используются для того, чтобы разбить нужным образом электронную таблицу на страницы, задавая горизонтальное и вертикальное разбиение. Это бывает важно при формировании отчетов и вывода результатов на печать. О них уже шла речь, при рассмотрении коллекции WorkSheets.
Объект PageSetup позволяет установить параметры страницы при выводе на печать.
Подводя итоги, заметим, что рабочий лист помимо того, что он представляет электронную таблицу ячеек, может содержать и другие элементы: диаграммы, рисунки, OLE -объекты. В нем могут быть также расположены сводные таблицы и таблицы, построенные на основе запросов к внешним источникам данных. Некоторые из ячеек рабочего листа снабжаются комментариями и имеют ссылки на внешние адреса. Ячейки и области данных могут иметь имена. Наконец, данные разрешается свернуть и отобразить структуру такого листа с нужной степенью подробности.
Терминальные и нетерминальные свойства объекта Range
Полное рассмотрение всех свойств объекта Range заняло бы слишком много времени, и я рассмотрю лишь группу основных, на мой взгляд, свойств, специфических для объекта Range.
Таблица 3.9. Свойства объекта Range
СвойствоОписание
Address, AddressLocals
Возвращает строку, задающую ссылку на Range объект. Во втором случае это ссылка в языке пользователя. Эту ссылку можно выдавать в формате A1 или R1C1, как абсолютную или относительную. Вид возвращаемого значения определяют параметры этого свойства (метода).
Areas
Применимо обычно к объекту Selection и возвращает коллекцию объектов Range в случае, когда Selection (Range) задает несвязную область. Возвращается сам объект Range, если область содержит только один объект.
Borders
Возвращает коллекцию из четырех границ объекта Range. Позволяет выделить цветом и (или) толщиной линии границы объекта.
Text, Characters
Свойство Text возвращает строку текста, связанного с Range объектом (ячейкой). Имеет статус только для чтения. Если нужно изменить весь текст или его часть, то можно использовать свойство (метод) Characters, два параметра которого: Start и Length позволяют выделить требуемую подстроку текста.
Column, Row
Возвращают соответственно номер первого столбца или первой строки в области объекта Range.
Font
Возвращает объект Font, используемый при написании текста в области объекта Range.
FormatConditions
Возвращает коллекцию условных форматов, содержащую не более трех элементов - объектов класса
FormatCondition.
Объект Range может иметь до трех условных форматов, выбор каждого из которых зависит от выполнения условия форматирования. Условие определяется параметрами объекта FormatCondition - оператором условия и константой, которая сравнивается со значением выражения, заданного объектом Range. В простейшем случае, когда объект Range задает ячейку, то значение в ячейке сравнивается с заданной константой. Метод Add коллекции позволяет задать новое условие форматирования. Методы Modify и Delete объекта FormatCondition позволяют модифицировать или удалять существующий формат. Параметры формата задаются с использованием объектов Borders, Font и Interior, возвращаемых свойствами объекта FormatCondition.
Первое из них позволяет прочесть или задать формулу в формате A1, второе - в формате R1C1, третье -формулу над массивами. Остальные также так или иначе связаны с заданием формул.
Locked
Возвращает значение True, если объект закрыт для модификаций и False, если модификация данного объекта возможно, хотя рабочий лист защищен. Возвращается Null, есди в области объекта Range существуют закрытые и открытые ячейки.
Offset
Об этом свойстве, возвращающем объект Range, у уже подробно рассказывал.
Style
Свойство имеет статус "только для чтения" - возвращает объект Style, характерный для объекта Range.
Value
Значение указанной ячейки. Если она пуста, то возвращается значение Empty, что можно проверить, вызвав функцию IsEmpty. Если объект Range содержит более одной ячейки, то возвращается массив значений, что можно проверить, вызвав функцию IsArray. Функции IsNumber, IsText позволяют определить тип значения, хранимого в ячейке.
Терминальные свойства объекта Chart
Основные терминальные свойства сведены в таблицу.
Таблица 3.8. Терминальные свойства объекта Chart
СвойствоОписание
ChartType
Позволяет прочесть или задать тип и формат стандартной диаграммы. Возможные значения задаются константами, которых около сотни. Напомним, что с каждым из 14 стандартных типов связано до 10 форматов.
AutoScaling
Булево свойство, имеющее значение True, когда трехмерная диаграмма автоматически масштабируется так, чтобы совпадать по размеру с двумерной. Свойство RightAngleAxes должно также иметь значение True.
BarShape
Мы ранее говорили, что двумерные гистограммы изображаются в виде прямоугольников. Для изображения трехмерных гистограмм обычно используются параллелепипеды, но можно применять и другие геометрические фигуры. Свойство BarShape задает вид используемой фигуры. Оно имеет следующие значения: xlBox, xlConeToMax, xlConeToPoint, xlCylinder, xlPyramidToMax, или xlPyramidToPoint. Использовать это свойство вряд ли стоит. Все эти фигуры- "изыски от лукавого".
DepthPercent, HeightPercent
Свойства применимы только к трехмерным диаграммам. Позволяют установить глубину и высоту диаграммы в процентах относительно ее ширины.
DisplayBlanksAs
Устанавливает способ интерпретации данных при встрече с пустой ячейкой. Следующие константы: xlNotPlotted, xlInterpolated, или xlZero задают три возможные стратегии- игнорировать ячейку, провести интерполяцию или считать нулем.
Elevation, Rotation, Perspective, RightAngleAxes
Можно попытаться повысить наглядность изображения диаграммы. Свойство Elevation задает возвышение (в градусах) точки, с которой Вы смотрите на диаграмму. Rotation задает поворот вокруг оси Z, а Perspective - перспективу. Булево свойство RightAngleAxes задает "угол зрения".
GapDepth
Задает в трехмерной диаграмме расстояние между рядами данных. Значение может быть в интервале от 0 до 500.
HasAxis, HasDataTable, HasLegend, HasTitle
Булевы свойства, показывающие, какие элементы диаграммы присутствуют в ней.
PlotBy
Имеет два значения: xlColumns и xlRows, указывающие столбцы или строки следует использовать как ряды данных.
PlotVisibleOnly
Булево свойство, имеющее значение True, если отображаются только данные из видимых ячеек. В противном случае диаграмма отображает все данные, включая скрытые ячейки.
Булевы свойства, позволяющие установить защиту соответствующих элементов диаграммы. Часть из них имеет статус "только для чтения".
ShowWindow
Булево свойство, применяемое только к встроенным диаграммам. Имеет значение True, если диаграмма отображается в отдельном окне.
Visible
Напомним, имеет три значения: True, False и xlVeryHidden.
Терминальные свойства объекта Workbook
Терминальных свойств, как обычно, множество. Они проще, чем свойства, задаваемые объектами. Среди них достаточно много булевых свойств, позволяющих включать или отключать то или иное свойство рабочей книги. Я приведу сводку некоторых из этих свойств, позволяющую получить общее представление о том, как можно управлять характеристиками рабочей книги Excel с помощью этих свойств.
Таблица 3.6. Терминальные свойства объекта Workbook
Терминальные свойстваНазначение свойства
AcceptLabelsInFormula
Булево свойство со значением True, если метки могут использоваться в формулах рабочего листа. По умолчанию - True.
HasRoutingSlip
Булево свойство со значением True, если книга может быть направлена по сети другим участникам разработки документа.
Routed
Булево свойство со значением True, если документ был направлен следующему участнику разработки.
MultiUserEditing
Булево свойство со значением True, если книга открыта для разделяемого доступа.
AutoUpdateFrequency
Задает частоту (в минутах), с которой сделанные изменения передаются участникам разделяемого доступа. Если свойство имеет значение 0, то книга будет пересылаться только в момент ее сохранения
AutoUpdateSaveChanges
Булево свойство со значением True, если сделанные изменения автоматически пересылаются всем участникам разработки. Предыдущий параметр должен иметь значение в пределах от 5 до 1440, чтобы это свойство оказало эффект.
ListChangesOnNewSheet
Булево свойство со значением True, если сделанные изменения показываются на отдельной странице при разделенном доступе
KeepChangeHistory
Булево свойство со значением True, если при разделенном доступе сохраняется история сделанных изменений.
ChangeHistoryDuration
Устанавливает число дней, в течение которых сохраняются изменения в их истории. Старые изменения, срок которых превышает заданную установку, из истории удаляются
CreateBackup
Булево свойство со значением True, если при сохранении книги создается ее резервная копия.
CodeName
Рабочие книги, листы и другие объекты Excel имеют два имени - собственное и кодовое. В момент создания объекта они совпадают, например "Лист1", но затем каждое из них может быть независимо изменено. Кодовое имя может быть изменено только вручную в окне свойств. Программно оно может быть использовано только для чтения. Важно то, что кодовое имя можно использовать для непосредственного именования объекта, что сокращает цепочку вызовов. Так непосредственно можно обратиться к объекту Лист1.Range(myRange)
FullName
Полное имя рабочей книги, заданное в виде строки, включающее путь к файлу, хранящему книгу. Имеет статус "только для чтения".
FileFormat
Свойство имеет статус "только для чтения" и возвращает константу, задающую формат файла и/или тип рабочей книги
HasPassword
Булево свойство со значением True, если документ имеет пароль защиты
Saved
Булево свойство со значением True, если не делалось никаких изменений с момента последнего сохранения документа.
WriteReserved
Булево свойство со значением True, если документ закрыт для записи.
Новые терминальные свойства объекта Workbook в Excel 2000
EnvelopeVisible
Новое в Excel 2000 терминальное булево свойство, при включении которого появляется панель для отправки электронной почты и заголовок отправляемого сообщения.
VBASigned
Новое в Excel 2000 терминальное булево свойство, которое показывает, имеет ли программный проект данной книги цифровую подпись. Свойство имеет статус "только для чтения".
Приведу две простые процедуры, которые демонстрируют использование некоторых терминальных свойств. Первая из этих процедур выводит на печать имена рабочей книги и путь к ней:
Public Sub AllNames() 'Печать имен документа With ThisWorkbook Debug.Print "Свойство Name - ", .Name Debug.Print "Свойство CodeName - ", .CodeName Debug.Print "Свойство FullName - ", .FullName Debug.Print "Свойство Path - ", .Path End With End Sub
Вот как выглядят результаты в окне отладки, полученные в результате работы этой процедуры:
Следующая процедура позволяет по желанию пользователя включить или отключить панель для отсылки почтового сообщения, используя новое свойство EnvelopeVisible:
Public Sub EnvelopeOn() 'Включение и выключение панели и заголовка почтового сообщения Dim Answer As Long Answer = MsgBox("Включить панель отправки почтового сообщения?", _ vbYesNo) If Answer = vbYes Then ThisWorkbook.EnvelopeVisible = True Else ThisWorkbook.EnvelopeVisible = False End If End Sub
Терминальные свойства объекта WorkSheet
Перейдем теперь к рассмотрению основных терминальных свойств объекта Worksheet. Представим их, как обычно, таблицей:
Таблица 3.7. Терминальные свойства объекта WorkSheet
Терминальные свойстваНазначение свойства
CodeName, Name, Index
Свойство CodeName имеет статус только для чтения и позволяет установить кодовое имя рабочего листа. Мы уже говорили, что рабочие книги, рабочие листы и листы диаграмм наряду с именем имеют и кодовое имя. Свойство Name позволяет задать или изменить имя рабочего листа. Это свойство, также как и свойство Index, имеют многие объекты. Index позволяет по имени объекта получить его порядковый номер в коллекции.
Excel имеет разные способы агрегирования данных. Мы уже говорили об объекте Outline, позволяющем структурировать данные, представляя их с разной степенью детализации. Другим средством являются сводные таблицы. Еще одну возможность объединения данных дает их консолидация. Как правило, консолидируются однотипные данные, построенные, например, на основе единого шаблона. Можно, например, консолидировать данные, представляющие результаты работы однотипных подразделений. Что реально скрывается за термином "консолидация" определяет функция консолидации - чаще всего это функция Sum, проводящая обычное суммирование. Но это может быть и нахождение среднего или минимального (максимального) значения.
Свойство ConsolidationFunction предназначенное только для чтения возвращает константу, задающую код функции консолидации: xlAverage, xlCount, xlCountNums, xlMax, xlMin, xlProduct, xlStDev, xlStDevP, xlSum, xlVar, или xlVarP.
Свойство ConsolidationSources возвращает массив строк, содержащий имена листов, служивших источниками для консолидации данных.
Свойство ConsolidationOptions возвращает трехэлементный массив булевых переменных, каждая из которых имеет значение True, если одна из трех соответствующих опций установлена. Об опциях и некоторых подробностях консолидации мы еще поговорим при рассмотрении метода Consolidate, которым обладает объект Range.
Для защиты книги или ее листа от случайных изменений используется метод Protect. В момент его вызова можно установить, какие именно элементы будут защищены.
Имеющие статус " только для чтения" булевы свойства: ProtectContents, ProtectDrawingObjects и ProtectScenarios имеют значение True, если соответственно защищены такие элементы рабочего листа, как ячейки, графические объекты, сценарии. Если свойство ProtectionMode имеет значение True, то это означает, что макросы, представляющие часть пользовательского интерфейса, доступны для изменений. Для того чтобы это свойство было включено, необходимо, чтобы при вызове метода Protect его параметр UserInterfaceOnly был задан и получил значение True.
Булевы свойства Enable включают или выключают ту или иную опцию.
EnableCalculation включает автоматическое вычисление формул, всякий раз, когда возникает необходимость, например, изменились данные.
EnableOutlining включает показ символов структуризации на защищенном листе при условии включения параметра UserInterfaceOnly.
EnablePivotTable как и предыдущий параметр включает элементы управления сводной таблицей на защищенном листе.
EnableAutoFilter включает стрелки автофильтрации на защищенном листе.
EnableSelection не является булевым параметром. В его задачу входит включить или выключить доступ к тем или иным ячейкам защищенного листа. Он имеет три возможных значения: xlNoRestrictions, xlNoSelection, or xlUnlockedCells. В первом случае для выбора доступны все ячейки, во втором - ни одна, в третьем - только "открытые" ячейки, у которых свойство Locked имеет значение False.
ScrollArea
Возвращает или устанавливает строку, задающую область рабочего листа, где разрешена прокрутка. Ячейки, расположенные вне этого диапазона не могут быть выбраны. Вот пример: Public Sub StrictScrolling() 'Ограничение области скроллинга! Dim S As String S = "A1:G20" With ThisWorkbook.Worksheets(2) 'Установка области прокрутки .ScrollArea = S '.ScrollArea = "" 'Попытка выделить ячейку вне области .Range("B21").Select Selection.Value = 77 End With End Sub
Заметьте, программно мы смогли выделить и записать значение в ячейку B21, находящуюся вне области прокрутки, но вручную ни эта ячейка, ни другие не будут доступны. В дальнейшем можно переопределить область прокрутки и, чтобы сделать доступными все ячейки, нужно задать пустую строку в качестве значения ScrollArea.
Замечу, что в отличие от предыдущей версии значение ScrollArea теперь можно задавать не только константой, но и переменной, как в нашем примере.
StandardHeight, StandardWidth
Первое из этих свойств возвращает стандартную (по умолчанию) высоту всех строк, второе - ширину всех столбцов. Разница между ними состоит в том, что первое имеет статус "только для чтения". Вы можете задать ширину всех столбцов, принимаемую по умолчанию. Для всех строк задать высоту нельзя, но это можно делать для каждой отдельно взятой строки, используя ее свойство - RowHeight.
TransitionExpEval, TransitionFormEntry
Эти булевы свойства обеспечивают совместимость с Lotus 1-2-3. Если они имеют значение True, то используются правила вычислений выражений и формулы Lotus 1-2-3.
Visible
В общем случае булево свойство, имеющее значение True, если объект виден. Но для рабочего листа имеется третье значение - xlSheetVeryHidden. В этом случае пользователь не сумеет вручную добраться до "очень" скрытого листа. Это можно будет сделать только программно, изменив его значение на True или False.
Терминальные свойства
Терминальных свойств много, и понятно почему. Приложение Excel, как и другие приложения Office 2000, могут быть настроены пользователем по своему усмотрению. Эту настройку можно выполнять вручную, а можно и программно. Настройка вручную большей частью проводится из меню Сервис | Параметры, используя возможности, предоставляемые различными вкладками в открывающемся окне параметров. Для программной настройки используются терминальные свойства, - в этом их основное назначение. Естественно, я не буду останавливаться на всех свойствах, - они просты. В ниже приведенном обзоре представлено выборочное описание некоторых групп терминальных свойств:
Группа свойств, задающих свойства приложения по умолчанию, - DefaultFilePath, DefaultSaveFormat, DefaultSheetDirection, - путь по умолчанию, формат по умолчанию, направление просмотра текста (слева направо или справа налево), задаваемое для некоторых языков. К этим же свойствам примыкает и ранее упоминавшееся свойство DefaultWebOptions.
Группа булевых свойств, позволяющих включить или выключить отображение на экране тех или иных элементов приложения - DisplayAlerts, DisplayCommentIndicator, DisplayFormulaBar, DisplayStatusBar и другие Display-свойства. Первое из этих свойств позволяет управлять выдачей на экран некоторых сообщений в процессе работы макросов, второе - отображать специальный индикатор при показе комментариев. Более часто приходится использовать управление показом панелей формул и статуса. Особенно часто приходится использовать эти свойства, когда документ Excel используется в специальных целях, например, при отображении различных бланков, когда внешний вид документа ничем не напоминает привычную электронную таблицу. Замечу, что используемое в этих случаях свойство DisplayGridLines, позволяющее отключать сетку, принадлежит объекту Windows, а не объекту Application.
Группа булевых свойств, позволяющих включить или выключить те или иные свойства - EnableAnimations, EnableAutoComplete, EnableCancelKey, EnableEvents, EnableSound. Первое из этих свойств позволяет управлять анимацией при добавлении или удалении строк и столбцов рабочего листа, второе - автозаполнением ячеек таблицы. Свойство EnableCancelKey не является булевым, оно принимает значения, заданные соответствующим перечислением, и позволяет управлять процессом прерывания программы при нажатии комбинации клавиш Ctrl+Break. Значение xlInterrupt, принятое по умолчанию, позволяет прервать выполнение макроса и перейти в режим отладки, где возможно пошаговое выполнение. Однако с помощью этого свойства можно задать разные режимы, как, например, передачу управления обработчику ошибок в момент прерывания. Пользоваться этим свойством следует осторожно, поскольку при зацикливании может возникнуть ситуация, когда нельзя будет прервать программу, не применяя грубых способов. Свойство EnableEvents позвол яет управлять включением событий объекта Application, а свойство EnableSound управляет включением звука в процессе работы приложений Office 2000.
Группа свойств, управляющих размерами главного окна приложения Excel - Height, Width, Left, Top, задающие высоту, ширину окна и координаты верхнего левого угла окна.
Многие другие свойства, позволяющие управлять курсором, скроллингом, характеристиками пользователя и многими другими параметрами так или иначе, характеризующими приложение Excel.
Внешние ссылки, Web-запросы и событие Change
Согласно документации, событие Change возникает при изменениях значений в ячейках рабочих страниц, производимых пользователем или внешней ссылкой. Это утверждение требует ряда уточнений.
Прежде всего, уточню, речь идет не об изменении значений, а о записи значения в ячейку рабочей страницы. Даже если значение не изменится, при записи все равно возникнет событие Change.
Изменения значений в ячейках, произошедшие в результате вычислений по формулам, связанных с этими ячейками, не приводят к возникновению события Change.
Напомню, что внешняя ссылка - это ссылка из одной рабочей книги на ячейку или, в общем случае, область другой рабочей книги Excel. Поскольку такие ссылки встречаются в формулах, то они не могут приводить к появлению события Change в том документе, где размещена такая внешняя ссылка.
Не только действия пользователя приводят к возникновению события Change - программные изменения состояния ячеек также являются причиной возбуждения этого события.
Подводя итог, уточню формулировку, сделанную в документации. Событие Change возникает при записи в ячейки рабочей книги, сделанные программно или пользователем. Вычисления по формулам в ячейках рабочей книги не приводят к возникновению этого события.
А теперь, в подтверждение сказанного приведу некоторые примеры:
Хочу обратить Ваше внимание на то, что при написании обработчика события Change объекта Application в качестве журнала изменений я выбрал список, помещенный в пользовательскую форму. Скажу честно, вначале я попытался использовать для этой цели отдельную страницу в одной из книг Excel, но эта попытка оказалась безуспешной и привела к зацикливанию. Дело объясняется тем, что, как Вы понимаете, не только действия пользователя приводят к возникновению события Change. Когда обработчик этого события записывает информацию о возникшем событии в журнал изменений - на одну из страниц рабочей книги, то автоматически возникает событие Change, обработка которого приведет к возникновению нового события и так далее до бесконечности. Этот пример подтверждает сделанный вывод - не только действия пользователя по изменению значений в ячейках рабочей книги приводят к возникновению события Change, - но и программное изменение состояния ячеек, в том числе производимое самой системой, также вызывает событие Change. Замечу в заключение, что вместо формы, для журнала изменений можно было бы выбрать документ Word, таблицу Access, файл, но только не страницу рабочей книги. Обратите также внимание, что для обработчика события Change, связанного с рабочей страницей - объектом WorkSheet, для журнала изменений можно использовать рабочую страницу, но, учтите, только страницу, не заданную данным объектом WorkSheet.
А теперь рассмотрим пример изменений значений в ячейках рабочей страницы, не приводящих к возникновению события Change. В книге BookThree в ячейках G1 и H1 на странице Лист1, для которой, как Вы помните, предусмотрен свой обработчик события Change, я записал соответственно формулы: = [BookOne.xls]Лист1!A1 = Лист2!A1 + Лист3!A2
"Знакомые" методы
Вначале рассмотрим методы объекта Worksheet, действие которых так или иначе уже было описано. Это позволит нам избежать некоторых подробностей:
Activate - активизирует рабочий лист.
Delete - удаляет объект. Этот метод, также как и нижеследующие методы Copy, Move, Select и другие рассматривались при описании работы с коллекцией листов. Конечно, именно при работе с отдельным листом эти методы чаще всего применяются.
Copy - имеет два варианта. В первом варианте используется без параметров, копируя содержимое рабочего листа в буфер. Во втором варианте Copy(Before, After) создает копию листа, помещая ее перед или после листа, вызвавшего метод. Позиция вставляемого листа задается параметрами метода. Понятно, что только один из двух параметров - Before или After - может быть задан.
Move(Before, After) - перемещает рабочий лист, изменяя порядок следования листов в рабочей книге.
Paste(Destination, Link) -помещает содержимое буфера на рабочий лист. Возможный параметр Destination указывается только тогда, когда содержимое буфера представляет некоторый диапазон. В этом случае параметр задается объектом Range, определяющим диапазон, в который будет помещено содержимое буфера. Второй возможный булев параметр Link получает значение True в случае, когда устанавливается связь с источником данных. По умолчанию имеет значение False. Одновременно можно задать только один из этих параметров.
PasteSpecial(Format, Link, DisplayAsIcon, IconFileName, IconIndex, IconLabel) - также помещает содержимое буфера в область выделения рабочего листа. Разница состоит в том, что метод применяется тогда, когда содержимое буфера хранится в специальном формате, отличном от формата Excel. Чаще всего метод применяется для размещения объектов других приложений. Параметр Format в виде строки задает формат объекта, хранящегося в буфере. Параметр Link имеет тот же смысл, что и в предыдущем случае. Остальная группа параметров используется тогда, когда объект "приклеивается" в виде значка. В этом случае параметр DisplayAsIcon имеет значение True, параметр IconFileName задает имя файла, содержащего значки, IconIndex - индекс значка в файле, IconLabel - текст, связанный со значком. Обратите внимание, что поскольку объект или значок помещается в область выделения, то предварительно такая выделенная область
Xі должна быть установлена. Рассмотрим пример помещения текста, взятого из буфера, в ячейку нашей тестовой книги BookOne. Сам текст был создан в приложении Word, - я поместил в буфер начало этого абзаца.
Public Sub PasteTextFromWord() 'В приложении Word некоторый текст документа 'был сохранен в буфере. В данной процедуре 'текст из буфера помещается в ячейку Excel. With ThisWorkbook.Worksheets(2) 'Установка области выделения .Range("B25").Select .PasteSpecial Format:="Microsoft Word 9.0 Document Object" .Range("B35").Select .PasteSpecial Format:="Microsoft Word 9.0 Document Object", _ DisplayAsIcon:=True End With End Sub
Вот как выглядит рабочий лист Excel после вставки из буфера текста, скопированного в приложении Word.
Рис. 3.6. Рабочий лист Excel с текстом документа Word при копировании из буфера
Продолжим рассмотрение методов:
Select(Replace) - создает объект Selection. Возможный булев параметр Replace имеет значение True, если новый объект заменяет ранее существовавшее выделение, и False, когда происходит расширение области выделения, так, чтобы она охватывала и новый объект.
SaveAs - сохраняет изменения, сделанные на рабочем листе в отдельном файле. Мы не будем перечислять все возможные параметры этого метода.
PrintPreview и PrintOut - позволяют осуществит предварительный просмотр и печать содержимого рабочей книги
CheckSpelling - позволяет проверить правописание содержимого рабочего листа. Имеет ряд возможных параметров, позволяющих задать ряд опций, например, словарь, предоставленный пользователем.
Protect и Unprotect - позволяют защитить лист от несанкционированных изменений и снять эту защиту. Первый из методов имеет ряд параметров- Protect(Password, DrawingObjects, Contents, Scenarios, UserIntefaceOnly). Параметр Password задает пароль, а остальные являются булевыми и позволяют включить или отключить защиту той или иной части листа. Значение True у последнего параметра говорит о том, что на защищенном листе пользовательский интерфейс будет защищен, а макросы могут изменяться. Если этот параметр опущен, то и макросы будут защищены. По умолчанию защищены ячейки листа (Contents) и сценарии, но не графические объекты.
Мир объектов Excel 2000
База данных офиса "Родная Речь"
В качестве тестовой базы данных я рассмотрю базу данных гипотетического офиса "Родная Речь", который в дальнейшем буду называть офисом РР. Офис РР занимается издательской деятельностью и имеет отделы (группы), которые непосредственно готовят и издают книги, занимаются маркетингом и рекламой, распространением и сбытом книг. С издательством сотрудничают авторы, переводчики, книготоргующие организации.
Фильтрация записей
Под фильтрацией понимается выделение из таблицы записей, удовлетворяющих условиям запроса. Фильтровать записи можно как вручную, так и программно. В Excel есть два метода фильтрации записей списка: AutoFilter и AdvancedFilter. Первый фильтрует записи на том же месте, где находится сам список. Критерии отбора записей можно задать только для одного поля. В результате его работы исходный список делается невидимым, показываются только отобранные фильтром записи. При работе вручную можно просмотреть эти записи, в случае необходимости скорректировать их или скопировать. При программировании чаще используется метод AdavncedFilter, позволяющий не только фильтровать записи на месте, но и копировать результаты в указанное место. Важнее, что он позволяет задавать довольно сложные условия отбора записей, накладываемые, при желании, на все поля списка. Рассмотрим подробнее работу этих методов. Синтаксис метода AutoFilter таков: expression.AutoFilter(Field, Criteria1, Operator, Criteria2, VisibleDropDown)
Выражение Expression должно возвращать Range-объект, задающий список. Параметры метода имеют следующий смысл:
Field - задает порядковый номер поля списка, используемого для отбора записей; критерии отбора накладываются именно на это поле;
Criteria1 и Criteria2 - задают два возможных условия, накладываемых на поле. Если заданы оба эти параметра, то параметр Operator может принимать значение xlAnd или xlOr. Каждый из параметров представляет собой строку вида
<знак операции отношения><значение>",
где могут быть использованы все обычные знаки операции отношения: " >, >=, <, <=, =, <>". Если опущен знак операции, - подразумевается "равенство"; если опущено значение, а знак операции - "=" или "<>", условие означает проверку поля на пустоту. Если параметры Criteria1 и Criteria2 опущены, то никакие условия не накладываются и выбираются все записи. Чаще всего опускается один параметр - Criteria2. В этом случае могут быть заданы дополнительные условия выборки. Можно выбрать первые N записей, имеющих максимальные или минимальные значения. Параметр Critria1 указывает тогда число N, а значение параметра Operator указывает, задает ли N число записей или число процентов записей, максимальные или минимальные значения которых будут отбираться.
Operator - принимает одно из значений: xlAnd, xlOr, xlBottom10Items, xlTop10Items, xlBottom10Percent, xlTop10Percent; первые два используются, если заданы оба критерия, остальные задаются, если задан только первый критерий и он определяет не отношение, а специальные условия выборки записей. По умолчанию значение параметра Operator - xlAnd.
VisibleDropDown - булев параметр, значение True которого позволяет сделать видимым поле фильтрации. Для этого подсвечивается стрелка выпадающего списка, связанного с именем поля.
Если метод AutoFilter вызывается без параметров, он отключает результаты предыдущей фильтрации и список показывается полностью в обычном виде.
Я создал некоторый список и хочу поэкспериментировать с ним:
увеличить изображение Рис. 4.22. Список до начала фильтрации его записей
Я создал некоторый список и хочу поэкспериментировать с ним:
увеличить изображение Рис. 4.22. Список до начала фильтрации его записей
Ниже Вы видите процедуру, в которой проводятся эксперименты по фильтрации записей этого списка. Конечно, предполагается пошаговое ее выполнение, чтобы видеть результат каждой фильтрации.
Sub Автовыбор() 'Эксперименты с автофильтрацией Range("B1").Select 'Эксперимент 1. 'Выбор 3-х элементов с максимальными значениями 2-го поля. Selection.AutoFilter Field:=2, Criteria1:="3", _ Operator:=xlTop10Items, VisibleDropDown:=True
'Эксперимент 2. 'Типичное условие выбора с двумя критериями Selection.AutoFilter Selection.AutoFilter Field:=2, Criteria1:="<12", _ Operator:=xlOr, Criteria2:=">30", VisibleDropDown:=False
'Эксперимент 4. 'Будут выбраны 5 записей из списка Selection.AutoFilter Range("B1").Select Selection.AutoFilter Field:=2, Criteria1:="12", _ Operator:=xlOr, Criteria2:=">30" End Sub
Процедура подробно прокомментирована и, по-видимому, не нуждается в особых пояснениях. Взгляните на заключительные результаты ее работы:
увеличить изображение Рис. 4.23. Результаты фильтрации списка
Обратите внимание, в результате последней фильтрации из списка выбраны 5 записей, имеющие номера: 2, 3, 4, 7, 8. Только эти записи отображаются на рабочей странице, остальные записи недоступны, пока фильтр остается включенным.
Я хочу теперь рассказать еще об одном объекте, связанном с автофильтрацией. Напомню, что каждый список следует располагать на отдельном листе, и методы фильтрации могут быть применены только к единственному списку на листе. В терминах объектов это означает, что объект Worksheet имеет свойство AutoFilter, возвращающее одноименный объект. Объект AutoFilter хранит информацию о фильтрах, применяемых к списку. Главное свойство этого объекта - Filters возвращает одноименную коллекцию, элементы которой являются объектами класса Filter. Число элементов в коллекции совпадает с числом полей списка. Каждый из объектов Filter сохраняет параметры фильтра, применяемого к соответствующему столбцу списка. Я покажу на примере, какими свойствами обладают эти объекты. Кроме свойства Filters можно еще отметить и свойство Range объекта AutoFilter, возвращающее, по существу, область, занятую списком.
Вот пример работы с объектом AutoFilter:
Public Sub FilterAuto() 'Работа с объектом AutoFilter и результатами фильтрации Dim Myf As AutoFilter, filt As Filter 'Определить объект AutoFilter, связанный со страницей Set Myf = ThisWorkbook.Worksheets("Лист2").AutoFilter Debug.Print "Число фильтров = ", Myf.Filters.Count For Each filt In Myf.Filters Debug.Print "Включен = ", filt.On If filt.On Then Debug.Print "Фильтр:", filt.Criteria1, _ filt.Operator, filt.Criteria2 End If Next filt
Как видите, булево свойство On объекта AutoFilter позволяет определить, включен ли фильтр. Свойства Criteria1, Criteria2 и Operator возвращают одноименные параметры фильтра.
Давайте теперь разберемся еще с одним важным вопросом, - как программно можно получить и использовать результаты фильтрации. Прежде всего, взгляните на процедуру, которую я разработал, чтобы продемонстрировать возможный подход к решению этой задачи:
Public Sub FilterRes() 'Работа с результатами фильтрации Dim Myf As AutoFilter Dim Myr As Range, Myr1 As Range Dim MyrBeg As Range, MyrEnd As Range Dim cel As Range 'Определить объект AutoFilter, связанный со страницей Set Myf = ThisWorkbook.Worksheets("Лист2").AutoFilter 'Область списка Set Myr = Myf.Range 'Отрезаю строку заголовков списка Set MyrBeg = Myr.Offset(1, 0).Cells(1, 1) Set MyrEnd = Myr.Cells(Myr.Rows.Count, Myr.Columns.Count) Set Myr = Range(MyrBeg, MyrEnd) 'Выделяю область фильтрации Myr.Select Myr.Copy 'Получаю на другом листе результаты фильтрации Set Myr1 = Range("Лист3!F2") Myr1.PasteSpecial Set Myr1 = Myr1.CurrentRegion 'Объект Myr содержит все данные списка, 'Объект Myr1 содержит только отфильтрованные данные. Debug.Print "Число ячеек исходной области = ", Myr.Cells.Count Debug.Print "Число ячеек области фильтрации = ", Myr1.Cells.Count 'Обработка результатов фильтрации For Each cell In Myr1.Cells Debug.Print cell Next cell
End Sub
Цель этой программы показать, как можно получить результаты применения фильтра и программно работать с ними. Для этого я первым делом получаю в объекте Myr область, занятую списком, для чего использую метод Range объекта Autofilter. Затем я отрезаю строку, занятую заголовками списка, после чего копирую содержимое объекта Myr в буфер. Заметьте, хотя объект Myr содержит всю область списка, в буфер передается только видимая часть списка - результаты фильтрации. Так что, скопировав результаты из буфера в объект Myr1, я получаю результаты фильтрации и могу с ними работать, так как мне нужно, - для простоты я их просто печатаю. Обратите внимание, результаты я копирую на новый лист рабочей книги, иначе они будут восприниматься как список. Таким образом, в объекте Myr у меня хранятся результаты до фильтрации, а в объекте Myr1 - после фильтрации. Вот результаты отладочной печати:
Число ячеек исходной области = 32 Число ячеек области фильтрации = 20 Anna 12 5 low Mary 12 7 low Anna 12 5 low Петр 35 18 middle Петр 37 18 high
Импорт списков Excel в приложении Access
Приложение Access позволяет импортировать внешние данные, и я воспользуюсь этой возможностью для переноса списков Excel. Я создал в Access новую, пока что пустую базу данных, дал ей имя "dbPPnew" и занялся выполнением операции импорта, выбрав из меню "Файл" команду "Внешние данные | Импорт…". В открывшемся окне Импорта я, как обычно, в окошке "тип файла" выбрал из большого раскрывающегося списка нужный мне тип - Microsoft Excel - затем в поле "Папка" выбрал нужную папку, выбрал файл с книгой Excel, содержащей базу данных и нажал кнопку "Импорт". В результате, Мастер Импорта начинает свою работу. Вот первое окно, которое открывает этот Мастер, предлагая импортировать рабочие листы или именованные диапазоны книги Excel:
Рис. 4.18. Импорт списков Excel Обратите внимание, я предпочел работать с именованными диапазонами, а не с листами книги. Дело в том, что Мастер импорта не слишком интеллектуален и не может разобраться, где на рабочем листе начинается список Excel. Он предполагает, что заголовки полей списка начинаются в первой строке. Я же рабочий лист начинал с некоторого общего заголовка, и только потом уже размещал список. По этой причине, прежде чем заниматься импортом списков, я ввел именованные диапазоны для списков, назвав каждый диапазон по имени списка. Это позволит Мастеру Импорта разобраться с именами полей списка и сделать их именами полей таблицы Access, при условии, что на втором шаге работы Мастера будет включен флажок "Первая строка содержит заголовки столбцов". Я включил этот флажок, а на третьем шаге работы включил переключатель "В новой таблице", поскольку речь идет не о добавлении данных в существующую таблицу, а о создании новой таблицы. Вот как выглядит следующее окно Мастера Импорта, позволяющего уточнить характеристики полей таблицы:
Рис. 4.19. Мастер Импорта уточняет характеристики полей таблицы На следующем шаге можно указать Мастеру, какое поле является ключевым или добавить в таблицу поле счетчика, которое и будет играть роль ключа. Единственная проблема возникает в случае составного ключа, - Мастеру можно задать лишь одно ключевое поле, остальную работу по уточнению состава ключа придется выполнить уже в Access. Наконец, на последнем шаге работы можно указать не только имя таблицы, но и включить два флажка, один из которых вызывает Мастера Анализа таблиц, который позволяет провести проверку эффективности (с точки зрения этого Мастера) качества проектирования таблицы и определить целесообразность ее возможного разбиения на несколько таблиц.
Рис. 4.20. Последний шаг работы Мастера Импорта Я не стал вызывать Мастера Анализа таблиц, но надеюсь, что еще придет его время, и я расскажу подробнее о шагах его работы. Таблица "Книги" была успешно перенесена из Excel в Access. Аналогичным образом можно было бы импортировать и другие списки Excel, преобразуя их в таблицы базы данных Access. Но следующий список "Заказчики" я перенесу из Excel в Access, используя команду "Перенести в MS Access", которая появляется в меню Excel при включенной надстройке "AccessLinks".
Изменение данных в списке
Расширенный фильтр позволяет выделить из списка записи, удовлетворяющие некоторому критерию. Дальше с этими записями можно работать, используя обычные методы работы с Range -объектами. Но как быть, если необходимо провести корректировку выделенных записей. Понятно, что эту корректировку чаще всего нужно делать непосредственно в базе данных, т. е. в исходном списке. Но у нас нет информации о том, какой порядковый номер в списке имеет первая выделенная запись. Как всегда, в таких ситуациях нечего надеяться на систему и следует самому позаботиться о себе. Включайте в каждый список первым полем порядковый номер записи (во многих таблицах обычная практика, когда первым идет счетчик). Тогда в каждой выделенной записи есть поле, задающее ее порядковый номер, а этого достаточно, чтобы добраться до записи в списке и изменить ее нужным образом. В заключение заметим, что если стандартных методов сортировки и фильтрации данных списков не хватает, то на VBA можно написать обработку любого сколь угодно сл ожного запроса. В последующих главах будут продемонстрированы способы работы с офисными документами, значительное внимание при этом будет уделено различным запросам на выбор данных, вопросам динамического пополнения базы данных в процессе появления новых документов, также как и использованию информации, хранящейся в базе данных для создания документов.
Экспорт таблиц Access
Если база данных в Access уже создана, то ее можно без труда экспортировать в Excel. Этим мы сейчас и займемся. Поскольку операции по экспорту можно выполнять по-разному, то я рассмотрю несколько способов. Начну с самого простого, основанного на классическом использовании буфера при переносе тех или иных данных из одного приложения Office 2000 в другое. Вот краткое описание моих действий по переносу таблицы "Сотрудники". Прежде напомню, что к моменту начала переноса таблицы у меня уже создана книга Excel, страницы которой будут хранить базу данных, и первая таблица этой базы была только что создана. Так что мне осталось сделать следующее:
Открыть БД офиса РР, сделанную на Access.
Открыть таблицу "Сотрудники" в режиме таблицы, выделить и скопировать в буфер ее содержимое.
Перейти на страницу "Сотрудники" рабочей книги Excel и выполнить операцию "Специальная вставка" из буфера, вставляя содержимое как текст в формате Unicode.
Отформатировать подходящим образом вставленную таблицу. Замечу, что для единообразия использовалось форматирование уже созданной таблицы "Книги".
Вся работа заняла несколько минут. Вот как выглядит таблица "Сотрудники" после ее переноса на соответствующий лист рабочей книги:
Рис. 4.15. Копирование таблицы "Сотрудники" из Access в Excel
Метод AdvancedFilter
Перейдем теперь к рассмотрению более полезного для программистов и в любом случае более универсального метода фильтрации записей - AdvancedFilter. Вот его синтаксис: Function AdvancedFilter(Action As XlFilterAction, [CriteriaRange], [CopyToRange], [Unique])
Это метод вызывается объектами Range и возвращает объект Range, задающий список. Параметры метода имеют следующий смысл:
Action - может принимать одно из двух значений: xlFilterInPlace, xlFilterCopy.; первое из них указывает, что список фильтруется на месте, второе - задает возможность копирования результатов на новое место. О недостатках фильтрации на месте я уже говорил, поэтому при программировании целесообразно копировать значения, - это облегчает доступ к выбранным записям.
CriteriaRange - задает область, в которой можно записать логическую формулу, задающую условие выбора записей. Заметьте, что здесь формула позволяет задать условия, одновременно налагаемые на все поля записи. О том, что собой представляет область критериев и как строится формула, задающая условие, я скажу чуть ниже.
CopyToRange - задает область копирования результатов фильтрации.
Unique - булев параметр, значение True которого позволяет отобрать только один экземпляр записи в случае, когда она многократно встречается в списке. Если параметр имеет значение False, то выдаются все имеющиеся экземпляры записи.
Чтобы понять, как работает метод, нужно четко представлять, что собой представляет область критериев и как в ней формируется условие выбора. Этим мы сейчас и займемся. Для создания области критериев нужно выбрать любую свободную область листа и задать в ней имена полей. Заметьте, в этой области имя одного и того же поля может появиться дважды. На следующем шаге следует задать логическую формулу, накладывающую ограничения на поля выбираемых записей. Для тех, кто знаком с логикой, скажем, что эта формула записана в дизъюнктивной нормальной форме и представляет собой дизъюнкцию конъюнктов. Каждый конъюнкт записывается в отдельной строке под именами полей. Все члены в одной строке соединены знаком конъюнкции "And", а отдельные строки - знаком дизъюнкции "Or". Теперь попробуем сказать то же самое, но проще. Условия выбора можно задавать в нескольких строчках. Если есть две строки и условие в первой из них обозначить через F1, а во второй - F2 , то общее условие будет иметь вид F1 Or F2. Сами условия F1 и F2 могут быть достаточно сложными. Они объединяют знаком "And" элементарные условия, накладываемые на каждое поле в отдельности. Элементарные условия - это известные нам по методу AutoFilter отношения ">, <, = и т. д.". Поэтому в области критериев имена полей могут встречаться дважды, чтобы была возможность задать условие вида "(Поле1 > 10) And (Поле1<= 20)". Из сказанного следует, что расширенный фильтр позволяет формировать сложные условия отбора записей из списков. При этом я рассказал еще не обо всех его возможностях. Так, для текстовых полей можно задать образец, которому соответствуют множество записей. При этом действуют обычные в таких случаях правила:
Если в образце используется символ "?", то в записи ему соответствует любой символ; образцу "к?т" соответствуют, например, записи с полями "кит" и "кот".
Если в образце используется символ "*", то в записи ему соответствует любая последовательность символов. Так, образцу "*он" соответствуют записи "он", "кон" и "Наполеон".
Чтобы символы "?" и "*" могли использоваться как обычные, им должен предшествовать символ тильда "~"; образцу "Кто~?" соответствует строка "Кто?".
Любому образцу соответствуют все строки, чьим префиксом он является. Так, образцу "Петр" соответствуют строки: Петр, Петрушка, Петрович. Чтобы задать точное соответствие, условие сравнения нужно ввести в виде: ="=Петр".
Внимательно взгляните на рисунок, где показан слегка видоизмененный список из предыдущего примера. Здесь же показаны две области, отведенные под критерии, и результаты двух запросов, реализуемых двумя вызовами метода AdvancedFilter:
увеличить изображение Рис. 4.24. Результаты работы расширенного фильтра
Приведем теперь процедуру, дважды вызывающую расширенный фильтр с различными областями критериев:
Sub РасширенныйВыбор() 'Сложное условие фильтрации. Range("МойСписок").AdvancedFilter Action:=xlFilterCopy, _ CriteriaRange:=Range("F7:K14"), _ CopyToRange:=Range("A13:D13"), Unique:=True 'Здесь условие проще. Range("МойСписок").AdvancedFilter Action:=xlFilterCopy, _ CriteriaRange:=Range("F16:G21"), _ CopyToRange:=Range("I16:L16"), Unique:=False End Sub
Первый вызов метода AdvancedFilter стоит обсудить подробнее. Метод вызывается объектом Range, задающим список. Как мы говорили ранее, список целесообразно именовать. В нашем примере его имя - "МойСписок". Результаты фильтрации копируются в область, заданную параметром CopyToRange. Размер этой области заранее не известен, но знать его и не нужно - достаточно указать диапазон для записи заголовков полей. Под строкой заголовков будут располагаться записи, выбранные при фильтрации. А вот область, отведенную для критериев, следует указать точно. При этом не следует ее именовать, как рекомендовано в документации. Во всяком случае, этого не стоит делать, если предполагается несколько различных запросов или они будут модифицироваться.
Что же записано в области "F7:K14", задающей условие выборки? В строке заголовков - имена полей, причем имена второго и третьего полей повторены дважды. Хотя исходный список содержит 4 поля, область критериев имеет 6 столбцов. Ниже строки заголовков расположены 7 строк, каждая из которых задает некоторое условие выбора записей. Выпишем явно условие, которое задают строки этой области. Вы можете проверить запись формулы, глядя на предыдущий рисунок. Конечно, для меня на самом деле исходной являлась эта формула, в соответствии с ней заполнялись ячейки в области критериев. Вот эта довольно сложная формула:
( (Поле3 >=5) And (Поле3 < 7) And (Поле4 = "low") ) OR ( (Поле1 = "Мария") And (Поле4 = "high") ) OR (Поле2 = 37) OR ( (Поле2 > 20) And (Поле3 < 20) And (Поле3 > 15) And (Поле4 ="middle") ) OR ( (Поле1 = "Анна") And (Поле4 = "high") ) OR ( (Поле1 = "Петр") And (Поле4 = "low") ) OR ( (Поле1 = "Алиса") And (Поле2 = 24) And (Поле4 = "low") )
Формула состоит из 7 дизъюнктов, каждый из них независимо добавляет новые записи в результирующую выборку. Рассмотрим их:
первый - выделяет из списка две одинаковые записи со значением Anna в первом поле. Однако, поскольку параметр Unique имеет значение True, выбираться будет только одна уникальная запись.
второй дизъюнкт выделяет из списка запись с именем Мария, имеющей в 4-м поле значение high;
третий - самый простой: он должен выделить записи, имеющие в поле 2 значение 37; такая запись есть в списке, это запись с именем Петр (high);
четвертый дизъюнкт правильно выделяет запись со значением middle -это запись с именем Петр (middle);
пятый - выделяет запись с именем Анна (high);
шестой дизъюнкт выделяет две записи с именами Петр (low) и Петрович (low);
седьмой - выделяет еще одну запись.
В результате этого сложного запроса из списка выделяется 8 записей из 9. По сути, выбраны все записи без дублирования. Следующий запрос мы не будем рассматривать подробно. Заметьте лишь, что в запросе изменено значение параметра Unique, и потому оба экземпляра продублированной записи появятся в результирующей выборке.
Перенос списков из Excel в Access
Специальная надстройка AccessLinks добавляет в меню Excel команды, позволяющие преобразовать списки Excel в объекты базы данных Access - таблицы, формы, отчеты. Если надстройка AccessLinks еще не подключена, то это следует сделать, выбрав команду "Надстройки" из меню "Сервис" и включив флажок соответствующей надстройки. При включенной надстройке в меню "Данные" появляются три команды: MS Access Form, MS Access Report, Convert to MS Access. Первые две из них позволяют по данным списка Excel построить форму и отчет базы данных Access, я не буду на них останавливаться, поскольку по существу все построение осуществляют известные в Access Мастера построения форм и отчетов. Давайте чуть более подробно рассмотрим лишь третью команду, преобразующую список в таблицу базы данных. Первое окно, которое появляется после вызова этой команды, позволяет указать базу данных Access:
Рис. 4.21. Первый шаг Мастера Преобразований списка Excel в таблицу Access А далее все возвращается на круги своя и работу продолжает уже знакомый нам Мастер Импорта, который и создает таблицу в базе данных Access. Замечу только, что поскольку работа начинается в Excel, то Excel способен распознать, где начинается список и передать точный список Мастеру Импорта без всяких пустых строк. Это, пожалуй, достаточно важная причина, по которой я рекомендовал бы перенос списков выполнять, используя именно этот способ работы. На этом я и закончу рассмотрение вопросов экспорта - импорта баз данных между Excel и Access.
Построение форм "Заказчики" и "Книги Редакции"
Таблицы можно просматривать и заполнять, не прибегая к формам. Но формы позволяют сделать этот процесс удобнее, надежнее и элегантнее. Формы необходимы, когда пользователь, работающий с таблицей, не должен видеть или заполнять все ее поля. Форма может обеспечить нужную "вырезку" полей таблицы. Поскольку визуальное построение форм в Access делается совершенно просто и понятно, то, опять-таки, я не буду сейчас останавливаться на деталях того, как это делается, и ограничусь рисунками уже построенных форм. Я выбрал разный внешний вид для форм " Заказчики" и "Книги Редакции". Вот как выглядит первая из этих форм:
Рис. 4.3. Форма "Заказчики" Для второй формы избран более "скромный", но более экономичный вид, позволяющий одновременно видеть на экране больше деталей:
Рис. 4.4. Форма "Книги редакции" Вот еще одна форма, построенная по таблице "Заказчики", позволяющая просматривать адреса заказчиков:
Рис. 4.5. Форма "Адреса заказчиков" В этой форме выводится лишь часть полей таблицы и, что более важно, запрещена корректировка данных таблицы, что легко достигается установкой соответствующих свойств формы. Вот как выглядят эти установки для нашей формы:
Рис. 4.6. Установка свойств формы "Адреса Заказчиков" Используя формы "Заказчики" и "Книги Редакции", я заполнил наши таблицы тестовой информацией - можно считать, первоначальная БД офиса создана. Конечно, такое непосредственное заполнение полей таблицы не является "правильным" способом создания информации, хранящейся в базе данных. Информация в базе данных офиса должна создаваться в момент создания соответствующих офисных документов. Позже я продемонстрирую такой естественный способ заполнения таблиц баз данных офиса РР на примере таблицы "Заказчики" и других таблиц, составляющих базу данных офиса РР.
Построение таблиц "Заказчики" и "Книги"
Access относится к реляционным базам данных, в которых основной формой представления данных являются таблицы. Таблицы связываются между собой за счет общих полей. Поскольку таблица еще и общечеловеческая форма представления данных, то ее создание интуитивно понятно всем. Запись (строку) таблицы можно рассматривать, как совокупность полей разного типа. Каждый столбец таблицы хранит значения одного поля. Так что каждая таблица задает некоторый набор записей, и каждая запись представляется одной строкой таблицы. Число полей записи однозначно определяет число столбцов таблицы. Access предоставляет различные средства, облегчающие задание структуры таблицы. Полагаю, что даже человек, не имеющий опыта работы с Access, работая в Конструкторе таблиц Access, без труда определит собственную таблицу. Отмечу лишь, что для каждого поля задается имя, выбирается его тип, и задаются другие свойства поля, например, описание. Свойства поля зависят не только от его типа, они включают такие характеристики, как указание на то, является ли поле индексируемым, имеет ли уникальное значение, должно ли обязательно присутствовать в записи. Они определяют также формат и маску, которой должно удовлетворять значение поля, и другие характеристики. Я не буду сейчас останавливаться на средствах Access, позволяющих создавать таблицы визуально, они достаточно просты. Для нас сейчас важнее понимать структуру таблиц, входящих в состав базы данных. Начну с определения таблицы "Заказчики", хранящей информацию о заказчиках офиса РР. Каждая ее запись включает следующие поля: Код заказчика, Название, Адрес, Город, Телефон, Директор, Прочее, Email. Вот как выглядит определение таблицы в конструкторе Access, где для каждого поля задаются имя, тип, описание и другие характеристики:
Рис. 4.1. Структура таблицы "Заказчики Обратите внимание, обязательным условием определения таблицы базы данных является задание ключа. Ключом может быть одно поле или совокупность полей, требуется лишь, чтобы значение ключа было уникальным для каждой записи. Это требование позволяет однозначно найти запись в таблице, зная ее ключ. Требование существования ключа записей всегда выполнимо, поскольку при необходимости есть возможность добавить к исходным полям записи поле счетчика, которое может выступать в роли ключа, автоматически давая записи новый номер при добавлении ее в таблицу. На рисунке 4.1 можно видеть (ключевые поля отмечены соответствующим значком -
), что в таблице "Заказчики" в роли первичного ключа выступает поле "Название". Заметьте, ключ можно определить далеко не единственным способом, например, поле "Код заказчика" также могло играть роль первичного ключа. Определим теперь структуру таблицы "Книги". Эта таблица содержит информацию о книгах, выпускаемых издательством "РР". Достаточно взглянуть на рисунок, чтобы увидеть, какие поля в нее включены:
Рис. 4.2. Структура таблицы "Книги" Две введенные таблицы "Заказчики" и "Книги" пока не связаны между собой, - они не имеют общих полей. Такая связь возникнет позже - при расширении базы данных. Она появится, когда заказчики начнут заказывать книги. Позже я добавлю в базу данных таблицу "Заказы", которая будет иметь общие поля, как с таблицей "Книги", так и с таблицей "Заказчики".
Построение запросов
Частью БД Access является совокупность "стандартных" запросов на поиск данных в базе. Запросы к базе данных могут возникать динамически в процессе работы пользователя и определяться его сиюминутными требованиями. Однако большое число запросов являются "стандартными" и их можно спроектировать заранее. В качестве примера я создал несколько простых запросов, которые можно считать стандартными. Таковым является запрос "Список Заказчиков", при реализации которого выбираются из таблицы "Заказчики" названия организаций и создается их полный список в алфавитном порядке. По сути, этот запрос сводится к выборке одного соответствующего поля из таблицы Заказчики и последующей сортировки отобранных значений. Так выглядит конструирование этого запроса:
Рис. 4.7. Конструирование запроса "Список Заказчиков" А так выглядит результат его выполнения:
Рис. 4.8. Результат выполнения запроса Поскольку Access транслирует создаваемые запросы в SQL форму, то предоставляется возможность просматривать и корректировать запросы и в этой форме. Результат трансляции нашего запроса: SELECT Заказчики.Название FROM Заказчики ORDER BY Заказчики.Название;
Приведу примеры еще нескольких запросов. Запрос "Список книг", построенный по таблице "Книги", позволяет для всех книг, данные о которых содержит таблица, создать список из двух полей, - автора и название книги. Список упорядочен по авторам. Приведу SQL-форму этого запроса: SELECT Книги.Автор, Книги.Название FROM Книги ORDER BY Книги.Автор;
Два предыдущих запроса были совсем простые, - при отборе записей не проводилась их фильтрация, не накладывалось никаких ограничений на значения полей записи. Создадим теперь запрос с фильтром. В запросе "Заказчики в Твери" используются два поля таблицы "Заказчики", и в список отбираются только те записи, у которых код города равен "Тверь". При показе результатов код города не выводится:
Рис. 4.9. Конструирование запроса с фильтром SQL-форма этого запроса имеет вид: SELECT Заказчики.Название FROM Заказчики WHERE (((Заказчики.Город)="Тверь")) ORDER BY Заказчики.Название;
Построенный только что запрос позволяет выбрать среди всех заказчиков тех, кто живет в Твери. Такой запрос является частным случаем общего запроса. Значительно чаще в подобных ситуациях строится запрос с параметром, в котором код города является параметром, задаваемым динамически в момент вызова запроса. Визуально запросы с параметрами строятся, как обычные. Чтобы предыдущий запрос превратить в запрос с параметрами, в поле запроса, где задавался город ("Тверь"), достаточно записать в квадратных скобках строку, запрашивающую имя параметра, например: [город?] или [Town]. Скобки - признак того, что задается параметр запроса, они сигнализируют о необходимости добавления элемента в коллекцию Parameters, связанную с запросом. Аналогично создаются запросы с несколькими параметрами.
Несколько слов о том, как вызываются запросы с параметрами. Если это делать визуально, то в момент вызова запроса для каждого из параметров будет открываться диалоговое окно с заданной при определении запроса строкой. В этом окне и следует задать значение параметра. При программном вызове значения параметров надо задать до начала вызова запроса, присваивая их соответствующим элементам коллекции Parameters. Взгляните, как конструируется запрос "Заказчики из города" с двумя параметрами:
Рис. 4.10. Запрос с двумя параметрами
Обычно при определении запроса строка, запрашивающая значение параметра, задается в вопросительной форме, что естественно при ее появлении в момент открытия диалогового окна. Поскольку значение строки не должно совпадать с именем поля, то и по этой причине целесообразно добавлять знак вопроса. Вот SQL запись данного запроса:
SELECT Заказчики.Название FROM Заказчики WHERE (((Заказчики.Город)=[город?]) AND ((Заказчики.Директор)=[директор?])) ORDER BY Заказчики.Название;
Заметьте, хотя в явном виде предложение Parameters в операторе SELECT не отображается, но соответствующая коллекция Parameters, связанная с запросом формируется.
Преобразование списков Excel в базу данных Access
Только что мы рассмотрели различные способы экспорта-импорта таблиц базы данных Access в списки Excel. Пожалуй, чаще приходится выполнять обратную операцию - переноса существующей базы данных Excel в базу данных Access. Необходимость в этом может возникать, например, в тех случаях, когда база данных Excel разрослась настолько, что дальнейшая работа требует более мощного инструментария Access. Иногда списки Excel используются просто для пополнения таблиц существующей базы данных Access. Понятно, что обратное преобразование данных Excel в Access выполняется сложнее по той простой причине, что база данных Access устроена значительно сложнее, чем списки Excel. Поэтому нет многих возможностей, которые есть при переносе данных из Access в Excel. Нельзя, например, сохранить таблицу Excel в виде mdb-файла в формате базы данных Access. Тем не менее, существуют, по крайней мере, два способа переноса данных. Первый из них позволяет начать работу по переносу данных в Excel, используя его команду "Перенести в MS Access", являющейся аналогом рассмотренной выше команды Access - "Связи с Office - Анализ в MS Excel". Второй способ использует возможности Access по импорту внешних данных. Я рассмотрю сейчас оба эти способа и выполню обратный перенос созданной базы данных офиса РР в Excel в базу данных Access, создав под другим именем новую копию существующей базы данных.
Сохранение в базе данных информации о заказах
Если бы база данных офиса РР хранила данные только о выпускаемых книгах и заказчиках, то полезность ее была бы невысока. Расширим эту базу данных за счет того, что будем хранить в ней данные о заказах, которые делают заказчики на книги, выпускаемые редакцией. Это расширение сразу же придает некий содержательный характер нашему тестовому примеру. Прежде всего, таблицы в базе данных станут взаимосвязанными, как им и полагается быть в любой приличной базе данных. С другой стороны на такой базе данных можно демонстрировать решение ряда типичных офисных задач. Обработка заказов, хранящихся в базе данных, позволяет руководству офиса в дальнейшем принимать обоснованные решения, например, назначать срок и объем последующего тиража, определять объем средств, вкладываемых в рекламную компанию, оценивать деятельность дилеров, осуществляющих заказы. Я предполагаю в последующих главах продемонстрировать некоторые возможности по обработке заказов, а пока давайте сосредоточимся на том, как сохранить данные о заказе в ба
зе данных. Каждый заказ, как правило, содержит требования на разные товары. В нашем случае это означает, что в одном заказе содержится заявка на книги разных авторов. В таких ситуациях целесообразно данные о заказе хранить не в одной, а в двух взаимосвязанных таблицах. Я назвал эти таблицы "Заказы" и "Заказано". В первой из них каждый заказ задается одной записью, хранящей общие сведения о заказе. Во второй таблице одному заказу соответствует несколько записей, - по одной записи на каждый заказываемый товар. Такой способ хранения информации в двух таблицах позволяет всю общую информацию о заказе сохранять лишь один раз. Есть целая наука о том, как правильно проектировать базы данных, но я сейчас не буду ее затрагивать, ограничившись лишь упоминанием о ней. Пока нам достаточно продолжить рассмотрение примера и определить структуру таблиц "Заказы" и "Заказано":
Заказы. Эта таблица имеет следующие поля: КодЗаказа, Заказчик, Сотрудник, ДатаЗаказа, Стоимость. Код заказа играет роль ключа и является уникальным полем, автоматически заполняемым в момент записи заказа в базу данных. Следующие два поля совпадают с соответствующими полями таблиц "Заказчики" и "Сотрудники" и, тем самым, связывают эти таблицы. Я неявно предполагаю, что база данных офиса РР уже частично расширена за счет естественного добавления таблицы "Сотрудники". Действительно, было бы странно хранить информацию о заказчиках и не иметь информации о собственных сотрудниках. Поэтому каждый заказ хранит информацию не только о заказчике, но и о сотруднике офиса РР, оформляющего этот заказ. Следующие два поля - ДатаЗаказа и Стоимость (общая стоимость заказа) не требуют особых пояснений. Так выглядит общая информация о заказе.
Заказано. Эта таблица имеет следующие поля: КодЗаказа, НазваниеКниги, Количество, Стоимость. Как я уже говорил, в этой таблице одному заказу будет соответствовать, как правило, несколько записей. Каждая запись - строка таблицы - содержит данные об одной из заказываемых книг. Все записи одного заказа будут иметь один и тот же код заказа, совпадающий с кодом заказа таблицы "Заказы". Поле "КодЗаказа" связывает между собой таблицы "Заказы" и "Заказано". Заметьте, повторяющийся код заказа не может быть ключом для таблицы "Заказано". Роль ключа в этой таблице играют два поля - КодЗаказа и НазваниеКниги, которые в совокупности являются уникальными для каждой записи таблицы "Заказано", хотя каждое из них в отдельности уникальным полем не является и может многократно повторяться в записях таблицы. Поля этой таблицы - Количество и Стоимость - задают количество экземпляров заказываемой книги и их суммарную стоимость.
Вот как выглядит определение этих таблиц в Конструкторе Access:
Рис. 4.11. Определение таблицы "Заказы" в конструкторе
Рис. 4.12. Определение таблицы "Заказано" в конструкторе
Теперь база данных офиса РР приобрела некоторый законченный вид. Первый этап ее создания можно считать завершенным. В ней есть запросы, есть формы, а главное созданы 5 таблиц: Книги, Сотрудники, Заказчики, Заказы, Заказано. Все таблицы базы данных можно связать между собой общими полями. Взгляните на схему, отражающую связи между таблицами:
Рис. 4.13. Схема связей между таблицами базы данных офиса РР
Заметьте, все связи между таблицами имеют тип "один ко многим". Так, например, одному коду заказа из таблицы "Заказы" соответствуют несколько записей с аналогичным кодом в таблице "Заказано". Аналогично, одной фамилии (полю ФИО) из таблицы "Сотрудники" соответствует несколько записей в таблице "Заказы", поскольку понятно, что один сотрудник может оформлять множество заказов.
Вот как выглядит определение этих таблиц в Конструкторе Access:
Рис. 4.11. Определение таблицы "Заказы" в конструкторе
Рис. 4.12. Определение таблицы "Заказано" в конструкторе
Теперь база данных офиса РР приобрела некоторый законченный вид. Первый этап ее создания можно считать завершенным. В ней есть запросы, есть формы, а главное созданы 5 таблиц: Книги, Сотрудники, Заказчики, Заказы, Заказано. Все таблицы базы данных можно связать между собой общими полями. Взгляните на схему, отражающую связи между таблицами:
Рис. 4.13. Схема связей между таблицами базы данных офиса РР
Заметьте, все связи между таблицами имеют тип "один ко многим". Так, например, одному коду заказа из таблицы "Заказы" соответствуют несколько записей с аналогичным кодом в таблице "Заказано". Аналогично, одной фамилии (полю ФИО) из таблицы "Сотрудники" соответствует несколько записей в таблице "Заказы", поскольку понятно, что один сотрудник может оформлять множество заказов.
Сортировка списков
Мы уже рассмотрели вопросы создания списков Excel, представляющих локальную базу данных, рассмотрели взаимное преобразование между списками Excel и объектами базы данных Access. Теперь пришла пора заняться специальными операциями, которые облегчают работу с данными, хранящимися в этой локальной базе данных. При этом я буду рассматривать, как визуальные методы работы со списками, так и программные. Сортировка списков - одна из основных операций, которые выполняются над данными, хранящимися в базе. Эту операцию, конечно, можно выполнять вручную. При программировании следует пользоваться методом Sort объекта Range. Приведем синтаксис этого метода: Function Sort([Key1], [Order1 As XlSortOrder = xlAscending], [Key2], [Type], [Order2 As XlSortOrder = xlAscending], [Key3], [Order3 As XlSortOrder = xlAscending], [Header As XlYesNoGuess = xlNo], [OrderCustom], [MatchCase], [Orientation As XlSortOrientation = xlSortRows], [SortMethod As XlSortMethod = xlPinYin])
Рассмотрим параметры метода и поясним его работу:
Key1, Key2 и Key3 - задают имена полей, по которым сортируется список, так что одновременно можно отсортировать список не более чем по трем полям.
Order1, Order2 и Order3 задают порядок сортировки - по возрастанию или убыванию независимо по каждому полю; значение для полей по умолчанию - "по возрастанию" (xlAscendinig).
Type - задается только при сортировке сводных таблиц и указывает тип сортируемых значений, задаваемых константами: xlSortLabels и xlSortValues.
Header - указывает, есть ли в списке строка с именами полей; если его значение - xlGuess, система сама должна определить, есть ли заголовки у полей (для этого они должны отличаться форматом).
Значение параметра OrderCustom, отличное от 1, используется, когда список сортируется в порядке, заданном пользовательским списком или некоторым стандартным, специальным списком, например, с названиями месяцев. Приведу пример, поясняющий ситуацию. Представьте себе, что значениями некоторого поля таблицы являются названия основных цветов радуги. Вы хотите отсортировать эти значения в порядке следования цвета в радуге: "красный, оранжевый, …, фиолетовый". Тогда следует создать пользовательский список, задающий порядок следования цвета, и добавить его в коллекцию списков методом AddCustomList: Application.AddCustomList ListArray:=Range("F1:F7")
Здесь предполагается, что область "F1:F7" содержит значения цветов радуги. При добавлении списка в коллекцию он получит свой номер, который и указывается, как значение параметра OrderCustom.
MatchCase (булев) - имеет значение False, если при сортировке регистр не учитывается, большие и малые буквы не различаются.
Orientation - задает ориентацию при перестановке сортируемых значений таблицы: сверху вниз (переставляются строки) или слева направо (переставляются столбцы).
SortMethod - позволяет выбрать порядок, заданный на символах алфавита. Он может задаваться соответствующей кодовой страницей или являться стандартным алфавитным порядком, в котором цифры предшествуют буквам, а латынь - кириллице.
Вот пример вызова этого метода для сортировки списка "Заказчики", показанного на рис. 4.17. Список сортируется по двум полям, - вначале упорядочиваются города, а затем организации каждого города:
Public Sub Sorting() 'Сортировка таблицы Заказчики по двум полям: 'вначале по полю "город", затем - "организация" Selection.Sort Key1:=Range("D5"), Order1:=xlAscending, _ Key2:=Range("B5"), Order2:=xlAscending, _ Header:=xlGuess, OrderCustom:=1, MatchCase:=False, _ Orientation:=xlTopToBottom End Sub
Следует заметить, что по окончании сортировки некоторые параметры метода - Orientation, OrderCustom и другие - сохраняют полученные значения. По этой причине разумно каждый раз заново устанавливать их значения, чтобы избежать возможных ошибок.
Создание базы данных в Excel
Сейчас я рассмотрю создание базы данных офиса РР непосредственно в Excel. Будут созданы те же таблицы с той же структурой, что и в базе, которая только что создавалась в Access. Я рассмотрю разные способы создания этих таблиц, в том числе путем экспорта таблиц Access. Первым делом я создал новую книгу Excel, в которой завел 5 рабочих листов по числу создаваемых таблиц, каждая страница именовалась по названию таблицы базы данных. Затем я перешел к созданию таблицы "Книги" на листе с одноименным названием. В процессе этой работы я следовал рекомендациям, приведенным в предыдущем параграфе. Работа это простая, и не думаю, что требуются хоть какие-либо дополнительные пояснения. Конечно, на этом этапе требуется выполнить подходящее форматирование для ячеек таблицы, но я не буду на этом останавливаться. Форматирование может быть и другим. Взгляните, что у меня получилось, и надеюсь, Вы сумеете сделать эту работу не хуже меня, в особенности с учетом того, что я не обладаю хорошим художественным вку сом.
увеличить изображение Рис. 4.14. Таблица "Книги", спроектированная в Excel
Создание в приложении Access базы данных офиса "РР"
Для пользователей Microsoft Office 2000 создание базы данных именно в Access самая естественная вещь. Этот параграф может служить предварительным знакомством с Access для тех, кто действительно не знаком с этим замечательным приложением. Создание базы данных в Access помимо прочего обладает двумя несомненными достоинствами:
Допускается создание и ведение базы данных простыми и интуитивно понятными средствами в визуальном стиле. Даже человек, далекий от этой весьма специфической области программирования, способен после предварительного непродолжительного знакомства начать создавать свою собственную базу данных.
В любом из приложений Office 2000 (Word, Excel, PowerPoint) легко получить доступ к БД Access.
В первом приближении БД Access можно рассматривать как совокупность взаимосвязанных таблиц с данными, запросов к ним, форм и отчетов, облегчающих ввод данных в таблицы и вывод информации из них в удобном для конечного пользователя виде, а также программных компонентов, выполняющих различные операции над данными. Построение БД офиса РР начну с введения минимально необходимых средств. По мере необходимости база будет расширяться.
Специальные средства экспорта таблиц Access в Excel
Вместе с тем Access имеет специальные, развитые средства экспорта таблиц БД и других своих объектов - запросов, форм, отчетов. Таблицу "Заказы" я переместил в Excel, используя следующий способ:
В окне базы данных Access выбрал имя таблицы "Заказы".
В меню Сервис выбрал команду "Связи с Office" и подкоманду "Анализ в MS Excel". Результатом ее выполнения стало автоматическое создание новой книги Excel с единственной страницей "Заказы", содержащей требуемую таблицу с подходящим форматированием. Заметьте, что команда Связи с Office имеет и другие подкоманды - Слияние с MS Word, Публикация в MS Word, - позволяющие передавать таблицу Access в приложение Word. Все подкоманды команды Связи с Office позволяют выполнять операции не только с таблицами, но и с частями таблиц, запросами, формами, отчетами.
Получив на предыдущем шаге нужную мне таблицу в Excel, я использовал далее средства Excel для перемещения таблицы в нужное место имеющейся у меня книги. После чего я применил собственное форматирование, чтобы сохранить единый стиль всех страниц моей книги, хранящей базу данных.
увеличить изображение Рис. 4.16. Экспорт таблицы "Заказы" с использованием команды Анализ в MS Excel Наконец, я рассмотрю еще один возможный способ экспорта таблицы Access, основанный на широких возможностях Access по сохранению его объектов в виде файлов самых различных форматов. Например, таблицы, запросы, формы, отчеты можно экспортировать в формат HTML. Таблицы и запросы можно экспортировать в БД, удовлетворяющие стандарту ODBC, их можно экспортировать в текстовые файлы, в ASP-страницы, в базы данных Paradox или dBase и во многие другие форматы. Естественно, среди форматов присутствуют и форматы файлов Excel различных версий. Вот как выглядят мои действия по экспорту таблицы "Заказчики":
Открыл БД Access и выбрал нужную мне таблицу "Заказчики".
В меню "Файл" выбрал команду "Сохранить как", а затем - "Экспорт".
В появившемся диалоговом окне Экспорта выбрал папку, согласился с предложенным именем файла, совпадающим с названием таблицы, в окошке "тип файла" из раскрывающегося списка, задающего многочисленные возможности по экспорту таблицы, выбрал тип "Microsoft Excel 97-2000". Затем включил флажок "Сохранить формат" и щелкнул кнопку OK. Хочу обратить внимание на то, что при выполнении этой операции следует быть осторожным поскольку, если задать имя существующего файла в папке, то все его содержимое заменится новой единственной страницей.
В результате этих действий автоматически создается новая книга Excel с заданным именем файла, на единственный лист которой и записывается сохраняемая таблица. Результат этих действий немногим отличается от предыдущего случая, когда использовалась команда Связи с Office.
Также как и в предыдущем случае экспорта таблицы, получив файл Excel, я занялся перемещением полученной таблицы, используя средства Excel для перемещения таблицы в нужное место имеющейся у меня книги. После чего применил собственное форматирование, чтобы сохранить единый стиль всех страниц моей книги, хранящей базу данных.
Вот окончательный результат моих действий:
увеличить изображение Рис. 4.17. Экспорт таблицы "Заказчики" Как видите, есть много способов, позволяющих перенести таблицы базы данных Access в списки Excel. Какой из них выбрать - дело вкуса. Скажу честно, я предпочитаю первый способ, когда таблица переносится через буфер без создания промежуточных файлов. То, что при этом может не сохраниться форматирование полей, не представляется особой бедой, поскольку после переноса таблицы в Excel все равно приходиться, как правило, заниматься форматированием, подбирая размеры строк и столбцов, шрифт, цвет и прочие атрибуты.
Списки Excel, как локальная база данных
Говоря об Excel и базах данных нельзя не упомянуть и о такой возможности Excel, как ведение собственной локальной базы данных, не использующей никаких внешних источников данных. Небольшие базы данных можно вести непосредственно на Excel. Такую базу данных можно представлять как совокупность таблиц. Таблицы естественны для Excel и их создание не вызывает никаких трудностей. Для поиска данных в таблицах, их отбору, сортировке предусмотрены специальные средства. Когда объем данных невелик и нет необходимости в сложных запросах, требующих одновременной работы с несколькими таблицами, работать с данными в Excel проще и быстрее. Умение вести базу данных на Excel особенно важно для тех, кто не имеет доступа к другим базам данных и работает с версией Office 2000, не содержащей Access. Базу данных Excel удобнее всего располагать на нескольких листах по числу таблиц, хранящих данные. На каждом листе обычно располагается одна таблица. Это ограничение накладывает метод AdvancedFilter, без которого, как правило, не обходится обработка данных в таблице. Многие запросы к таблице можно реализовать с помощью этого метода. Таблицу, хранящую данные, в Excel принято называть списком. Список, как и всякая таблица, состоит из строк и столбцов. Столбцы - это поля списка (таблицы БД). Первая строка списка обычно содержит имена полей. Вот несколько правил, которым должны удовлетворять списки, часть из них носит рекомендательный характер:
для имен полей следует задать формат (шрифт, цвет фона), отличный от формата ячеек, хранящих значения полей: это позволит системе автоматически отличать имена полей от их значений;
значения, хранящиеся в одном столбце (значения одного поля), должны иметь один и тот же тип;
рекомендуется именовать лист названием списка, возможно, с некоторым префиксом;
области ячеек, отведенной для списка (вместе с первой строкой, хранящей названия полей), целесообразно дать имя; эту область нужно выделить по максимуму, так чтобы она допускала дальнейшее пополнение списка;
список отделяется от остальной части рабочего листа пустыми строками и столбцами. Поэтому не рекомендуется, чтобы пустые строки и столбцы встречались в середине списка, т. е. записи, все поля которых пусты, нежелательны, - иногда это может привести к неприятностям.
Мир объектов Excel 2000
Интерфейс ODBC
Скажу несколько слов об интерфейсе ODBC . Он был создан для того, чтобы дать возможность приложению - Потребителю данных - получать данные из различных систем управления базами данных (СУБД - Data Base Management System - DBMS). При этом концептуальная схема работы такова - приложение через набор соответствующих API-функций, которые, собственно говоря, и составляют интерфейс ODBC, обращается к объекту, называемому Менеджер драйверов (Driver Manager). Последний, опять-таки через тот же набор API- функций обращается к драйверу источника данных. Замечу, что большинство существующих баз данных имеют соответствующие драйверы и, следовательно, являются ODBC-источниками данных. Следует также отметить, что языком запросов в этой модели выступает SQL. Взаимодействие четырех основных объектов: Потребителя, Менеджера Драйверов, Драйвера и Источника данных - и определяет концептуальную модель ODBC.
Интерфейс OLE DB
Целевая установка при разработке этого интерфейса была следующей. Решения в сфере бизнеса нужно принимать оперативно. Эти решения должны быть основаны на полной и достоверной информации. Конечно же, основным источником данных для принятия решений является корпоративная база данных, например, MS SQL Server или Oracle. Но только этих данных, как правило, недостаточно для принятия оперативного решения. Необходимо учитывать данные, хранящиеся в личных базах данных, таких как Microsoft Access или FoxPro. Необходимые данные необходимо уметь извлекать из систем электронной почты. Данные могут храниться в индексно-последовательных файлах системы Betrieve, может быть, просто в бинарных файлах, наконец, в документах Office 2000, например, в списках Excel. Все возрастающую роль, как источника данных, играют Web-страницы интернет. Чтобы обеспечить единообразный способ работы с разнообразными источниками данных и создавался интерфейс OLE DB, представляющий некоторое множество интерфейсов, основанных на стандарте COM (Component Object Model). Кратко опишу основные понятия (объекты), составляющие концептуальную и объектную модель OLE DB. Процесс работы с данными можно описать в терминах взаимодействия двух объектов - Поставщика данных (Provider) и Потребителя данных (Consumer). Потребителем является приложение, запрашивающее данные и непосредственно вызывающее функции, заданные интерфейсом. Поставщик данных или Провайдер - это приложение, экспонирующее функции интерфейса, - это тот посредник, стоящий между приложением, запрашивающим данные, и источником данных. Функциональные возможности набора COM-интерфейсов могут зависеть от Провайдера, от того, как тот или иной Провайдер реализует функциональность интерфейса для конкретного источника данных. Для наиболее часто используемых источников данных фирмой Microsoft, так и другими фирмами, разработаны ряд Провайдеров, ставших стандартами "де факто". Разработчики, создающие Провайдеров, отображают функциональность, присущую конкретному источнику данных в функциональность, определенную интерфейсом. Разработчики, создающие терминальное приложение - Потребителя данных, - вызывают функции, экспонируемые Провайдером, для получения доступа к данным. Позже я чуть подробнее расскажу о различных экземплярах объекта Provider, обеспечивающих связь с различными источниками данных. Провайдеры поддерживают не только непосредственный доступ к данным, но и другие службы (Services) - транзакции, удаленный доступ, структуризацию кэш-памяти (cache) и другие службы. Кэш-память - это промежуточная память, в которой происходит изменение данных, их обновление, удаление и добавление, пока не наступает момент фактического обмена данными между кэш-памятью и источником данных. Когда Потребитель связывается с Провайдером и обращается к нему за данными, то он должен вначале создать и инициализировать экземпляр объекта, задающий источник данных данного Провайдера (data source object). С помощью этого объекта можно создать второй центральный объект Провайдера - объект Session. Имея в своем распоряжении объект, задающий сеанс работы, можно уже создать и работать с собственно объектами, определяющими работу с данными - транзакциями, командами, наборами строк (объектами Transaction, Command, Rowset). Говоря о связывании Потребителя и Провайдера, следует упомянуть, что OLE DB определяет возможность использования адресов ресурсов - Uniform Resource Locators (URLs) как альтернативу связывания с помощью строки связывания (Connection String). Адреса URLs могут быть использованы для указания хранилищ данных, строк данных, потоков, наборов строк, объектов Session. Процесс связывания объекта OLE DB с ресурсом, именованным с помощью URL, называется прямым связыванием. При таком способе связывания можно даже и не инициализировать объект Session. На этом я закончу беглое знакомство с основными понятиями и концепциями OLE DB. Позже я более подробно остановлюсь на рассмотрении объектов верхнего уровня - объектах ADO, которые транслируются в объекты OLE DB, и объектная модель которых на более высоком уровне повторяет объектную модель OLE DB.
Методы объекта Command
У объекта Command всего три метода. Рассмотрим их:
Function Execute([RecordsAffected], [Parameters], [Options As Long = -1]) As Recordset. Запускает на выполнение команду, предписанную свойствами CommandText или CommandStream. В качестве результата возвращает либо ссылку на объект Recordset, либо поток, либо Nothing.
Параметры метода имеют следующий смысл:
RecordsAffected - переменная типа Long, возвращающая число записей, затронутых при выполнении операции. Этот параметр не дает числа записей результата. Для этого необходимо использовать свойство RecordCount.
Parameters - массив значений параметров типа Variant. Этот аргумент позволяет переопределить все или некоторые из параметров, заданных свойством Parameters объекта Command.
Options - значение, указывающее, как Провайдер должен транслировать текст входной строки или потока. Может быть битовой маской одного или более значений из перечислений CommandTypeEnum или ExecuteOptionEnum. Так, если результатом выполнения команды должен быть поток, то значение этого свойства следует задать как adExecuteStream.
Sub Cancel(). Прерывает выполнение команды Execute. Подробности уже рассматривались при описании объекта Connection.
Function CreateParameter([Name As String], [Type As DataTypeEnum = adEmpty], [Direction As ParameterDirectionEnum = adParamInput], [Size As Long], [Value]) As Parameter. Позволяет создать новый объект Parameter с заданными свойствами. Ссылка на этот объект и является возвращаемым значением. Параметры метода имеют следующий смысл:
Name - имя параметра,
Type -тип данных объекта Parameter, заданный перечислением DataTypeEnum. Вот лишь некоторые из возможных значений: adArray, adChar, adDate, adCurrency, adIUnknown.
Direction - тип самого объекта Parameter, заданный перечислением ParameterDirectionEnum. Он определяет является ли параметром входным или выходным параметром запроса или процедуры, возвращаемым значением хранимой процедуры Вот его возможные значения: adParamInput, adParamInputOutput, adParamOutput, adParamReturnValue, adParamUnknown.
Size - значение указывающее максимальную длину значения параметра в символах или байтах.
Value - значение типа Variant, указывающее значение объекта Parameter.
Метод создает, но не присоединяет автоматически созданный параметр к коллекции параметров. Это позволяет перед присоединением корректно установить необходимые свойства в коллекции Properties. Например, при задании типа данных как adNumeric следует также установить значения свойств NumericScale и Precision.
Методы объекта Connection
У объекта Connection 8 методов, позволяющих открыть и закрыть соединение, выполнить команду и прервать ее выполнение, методы, связанные с выполнением транзакции. Давайте рассмотрим описание этих методов:
Sub Open([ConnectionString As String], [UserID As String], [Password As String], [Options As Long = -1]). Один из центральных методов, с которого обычно начинается работа с источником данных, и который позволяет установить соединение с Провайдером. Первый параметр метода можно не задавать, если предварительно определить свойство ConnectionString. При задании этого параметра в момент открытия можно переопределить некоторые аргументы, заданные одноименным свойством. Два следующих параметра необходимы при установлении соединения с защищенным паролем источником данных. Последний параметр, задаваемый предопределенной константой, определяет некоторые характеристики устанавливаемого соединения.
Sub Close(). Метод закрывает открытое соединение. Он применяется не только тогда, когда надобность в соединении исчезает, а часто для того, чтобы изменить параметры соединения и вновь открыть его методом Open. Напомню, что ряд параметров соединения могут быть установлены только для закрытого соединения. Этим методом обладают целый ряд объектов ADO. Чтобы удалить соответствующий объект из памяти объекту нужно присвоить значение Nothing. Метод Close для этих целей не применяется, - он не удаляет объект.
Function Execute(CommandText As String, [RecordsAffected], [Options As Long = -1]) As Recordset. Объект Connection позволяет не только открывать соединение, но и выполнять операции над открытым источником данных. Метод Execute возвращает ссылку на созданный Провайдером объект Recordset, являющийся результатом выполнения команды, заданной первым параметром метода. Параметр CommandText, задающий описание команды, может быть текстом SQL-оператора, именем таблицы или хранимой процедуры, URL-адресом или текстом, специфическим для данного Провайдера. Второй параметр - RecordAffected возвращает число записей, затронутых при выполнении команды. Параметр Options позволяет задать дополнительную информацию для Провайдера, используемую при преобразовании текста параметра CommandText в реальную команду. Поскольку в результате выполнения команды возвращается объект Recordset, обладающий некоторым ограниченным набором возможностей, то для выполнения команд и получения объекта Recordset чаще используют объекты Command или Recordset.
Sub Cancel(). Большинство объектов ADO обладают этим методом. При его выполнении завершается асинхронный метод, то есть метод, для которого установлены свойства adAsyncConnect, adAsyncExecute или adAsyncFetch. Для объекта Connection завершаемым методом может быть метод Open либо Execute.
Function BeginTrans() As Long, Sub CommitTrans(),Sub RollbackTrans() - три метода для работы с транзакциями. Первый из них запускает новую транзакцию, второй - сохраняет результаты транзакции и завершает ее, третий - позволяет сделать откат, отменяя все изменения, сделанные во время текущей транзакции, сама транзакция при этом завершается. Если Провайдер поддерживает гнездованные транзакции, то повторный вызов метода BeginTrans в уже открытой транзакции начинает новую (гнездованную) транзакцию. Этот метод, вызванный, как функция, возвращает уровень гнездования транзакции. Замечу, что не все Провайдеры поддерживают механизм транзакций, не говоря уже о гнездованных транзакциях. Значение одного из атрибутов свойства Attributes объекта Connection можно задать так, что при закрытии текущей транзакции методами CommitTrans и RollBackTrans будет открываться новая транзакция.
Function OpenSchema(Schema As SchemaEnum, [Restrictions], [SchemaID]) As Recordset. Метод позволяет получить информацию от Провайдера о схеме базы данных. В качестве результата возвращается ссылка на объект Recordset, который будет открыт только для чтения со статическим курсором. Столбцы этого объекта содержат описание схемы. Возможные описания задаются спецификациями OLE DB, первый параметр метода определяет, какие именно столбцы появятся в объекте Recordset.
Методы объекта Recordset
У объекта Recordset много свойств, много и методов. С некоторыми из них мы уже знакомы, поскольку они появлялись в примерах. Теперь приступим к их систематическому рассмотрению. С помощью методов можно выполнять все необходимые операции над набором записей - перемещаться по набору, находить нужные записи, создавать новые записи и удалять существующие, менять содержимое записей и передавать состояние набора в базу данных. Начнем наше рассмотрение:
Sub AddNew([FieldList], [Values]), Sub Delete([AffectRecords As AffectEnum = adAffectCurrent]). Метод AddNew позволяет добавлять новые записи в набор. Конечно, для применения метода объект Recordset должен иметь статус обновляемого набора, что означает, что для него должен быть установлен соответствующий тип курсора. Можно проверить, допускает ли Провайдер добавление записей. Метод Support, о котором еще предстоит разговор, позволяет выяснить поддерживается ли Провайдером то или иное свойство. В данном случае для того, чтобы проверить возможность создания новых записей в наборе, его следует вызвать, задав в качестве аргумента константу adAddNew.
Что следует сделать, чтобы набор был обновляемым? Я напомню, что создать объект Recordset можно разными способами. Он создается при вызове методов Execute объектов Connection и Command, его можно создать, вызвав метод Open объекта Recordset. В примерах, которые я приводил ранее, это объект создавался при вызове методов Execute. Но, заметьте, объект, создаваемый таким способом всегда имеет статический тип курсора и не может обновляться. Для создания объекта Recordset, допускающего обновление, всегда нужно применять метод Open, задавая динамический тип курсора в момент открытия.
Метод AddNew можно вызывать без аргументов. В этом случае созданная новая запись становится текущей, ее поля можно заполнить обычным способом. Для того чтобы содержимое записи было перенесено в базу данных, следует вызвать метод Update. Можно предусмотреть возможность пакетного обновления и использовать метод UpdateBatch для передачи в базу данных группы записей. Приведу пример, в котором в базу данных добавляется новая запись и корректируется значение отдельных полей в некоторых записях: Public Sub CreateNewRecords() 'добавление и изменение записей базы данных Dim recExist As Boolean 'Создать соединение CreateConnection 'Создать команду 'задание свойств объекта Command Cmd1.ActiveConnection = Con1 Cmd1.CommandText = "Select * From [Книги]" Cmd1.CommandType = adCmdText 'Открытие обновляемого объекта Recordset With Rst1 .Open Source:=Cmd1, CursorType:=adOpenDynamic, _ LockType:=adLockOptimistic 'Узнаем характеристики набора Debug.Print .Supports(adAddNew) Debug.Print .LockType Debug.Print .CursorType 'Изменение записей recExist = False .MoveFirst Do While Not .EOF 'Обработка текущей записи If !Название = "Офисное программирование" Then recExist = True If ![Год издания] < 2000 And !Цена < 100 Then !Цена = !Цена * 2 .Update End If .MoveNext Loop If Not recExist Then .AddNew !Автор = "Владимир Биллиг" !Название = "Офисное программмирование" ![Год издания] = 2001 ![Число страниц] = 599 !Цена = 150 .Update End If End With End Sub
Заметьте, здесь метод AddNew, вызываемый без параметров, создал новую запись в наборе, она стала текущей, обычным способом заполнены поля этой записи, затем вызван метод Update для переноса записи в базу данных. Этот же метод использовался для изменения в базе данных значения поля "Цена" ряда записей.
При вызове метода AddNew можно задавать аргументы. Параметры метода имеют следующий смысл:
FieldList - имя поля или массив таких имен. Вместо имен можно задавать порядковые номера полей.
Values - значение поля или массив значений. Если аргументы задаются, то должны быть заданы оба аргумента, и они должны быть согласованы обычным образом - по числу элементов в массиве, тип значения должен также соответствовать типу поля.
Когда метод AddNew вызывается с аргументами, то создается запись с уже заполненными полями, для нее автоматически вызывается метод Update, так что нет необходимости вызывать его самостоятельно - эту заботу берет на себя система. Приведу пример такого способа добавления записей в набор и базу данных:
Public Sub CreateNewRecords2() 'добавление и изменение записей базы данных Dim recExist As Boolean 'Создать соединение CreateConnection 'Создать команду 'задание свойств объекта Command Cmd1.ActiveConnection = Con1 Cmd1.CommandText = "Select * From [Книги]" Cmd1.CommandType = adCmdText 'Открытие обновляемого объекта Recordset With Rst1 .Open Source:=Cmd1, CursorType:=adOpenDynamic, _ LockType:=adLockOptimistic
recExist = False .MoveFirst Do While Not .EOF 'Проверка существования записи If !Название = "Война и мир" Then recExist = True .MoveNext Loop If Not recExist Then .AddNew Array("Автор", "Название", "Год издания", _ "Число страниц", "Цена"), _ Array("Лев Толстой", "Война и мир", 2001, 799, 220) End If End With End Sub
На эти примеры я еще буду ссылаться при рассмотрении других методов объекта Recordset. Если метод AddNew добавляет записи, то обратный к нему метод Delete позволяет удалять записи из набора и соответственно из базы данных. Я не буду останавливаться на деталях, а продолжу рассмотрение других методов объекта Recordset.
Sub Cancel(), Sub CancelBatch([AffectRecords As AffectEnum = adAffectAll]), Sub CancelUpdate(). Эта группа методов позволяет отменить выполнение метода, в частности отменить проделанную работу по обновлению отдельной записи или пакета записей, если в процессе этой работы становится ясно, что вызывать соответствующий метод Update не следует. Метод CancelUpdate обычно вызывается, когда операция Update или Delete завершилась неуспехом, о чем можно узнать, анализируя свойство EditMode, указывающее на незавершенность состояния редактирования.
Function Clone([LockType As LockTypeEnum = adLockUnspecified]) As Recordset. Позволяет создать дубликат набора записей. Дубликат может иметь другие права доступа и использоваться, например, только для чтения.
Sub Open([Source], [ActiveConnection], [CursorType As CursorTypeEnum = adOpenUnspecified], [LockType As LockTypeEnum = adLockUnspecified], [Options As Long = -1]), Sub Close(). Метод Open является одним из основных способов создания объекта Recordset. Соответствующая объектная переменная, конечно, должна быть создана, а в момент открытия, используя параметры метода, создается реальный набор записей. Первые два параметра задают источник данных и активное соединение. Источником может быть ссылка на объект Command, SQL-оператор, хранимая процедура. В вышеприведенном примере в качестве источника задавался объект Command, заметьте, в этом случае задавать активное соединение не следует, оно определено в объекте Command. Два следующих параметра задают тип курсора и тип доступа к данным. О важности задания этих параметров я уже говорил, - без них не обойтись, при создании обновляемого набора записей. Последний параметр Options должен быть задан в тех случаях, когда первый параметр Source не является ссылкой на объект Command, в этом случае он задает способ интерпретации первого параметра, принимая значения констант из перечислений CommandTypeEnum и ExecuteOptionEnum. Приведу еще один пример открытия объекта Recordset, когда первый параметр не является объектом Command:
Public Sub CreateRst2() 'Создать соединение CreateConnection ' Открытие объекта Recordset With Rst1 .Open Source:="Select * From [Заказчики]", ActiveConnection:=Con1, _ CursorType:=adOpenStatic, Options:=adCmdText .MoveFirst Do While Not .EOF 'Печать поля записи Debug.Print !Название .MoveNext Loop End With End Sub
В этом примере источник данных задается SQL-оператором, а последний параметр указывает способ интерпретации, имея чаще всего используемое значение - adCmdText.
Метод Close позволяет закрыть ранее открытый объект, освобождая ресурсы.
Function CompareBookmarks(Bookmark1, Bookmark2) As CompareEnum. Поскольку внутреннее представление закладок зависит от Провайдера, то самому выполнять операции сравнения закладок невозможно. Для этих целей и используется метод CompareBookmarks, позволяющий сравнивать закладки, принадлежащие одному и тому же набору Recordset или набору и его клону. Фактически закладки передаются Провайдеру, который и возвращает результат сравнения. Значением результата является одна из констант перечисления CompareEnum: adCompareEqual, adCompareGreaterThan, adCompareLessThan, adCompareNotComparable, adCompareNotEqual.
Sub Find(Criteria As String, [SkipRecords As Long], [SearchDirection As SearchDirectionEnum = adSearchForward], [Start]). Если для выполнения операций сортировки и фильтрации используются свойства объекта Recordset, то для поиска используются его методы. Метод Find позволяет найти в наборе первую строку, удовлетворяющую критерию поиска, где критерий задается первым параметром метода. Критерий ограничен, - позволяет задать условие поиска только по одному полю, он задается строкой, имеющий следующий синтаксис: <Имя поля> Оператор <Значение поля>, где оператор может быть одной из операций сравнения либо оператором Like, задающим сравнение с шаблоном. Параметры SkipRecords и Start позволяют задать начало поиска. Первый из них указывает, сколько записей следует пропустить, начиная от текущей, чтобы начать поиск. Если задан параметр Start, то поиск начинается с записи, заданной этим параметром. Параметр SearchDirection указывает направление поиска, - вперед или назад от начала поиска. Поиск заверша ется, когда найдена запись, удовлетворяющая критерию, или поиск достиг конца набора записей. Так что неуспех можно определить по истинности EOF или BOF в зависимости от направления поиска. В случае успеха искомая запись становится текущей.
Function GetRows([Rows As Long = -1], [Start], [Fields]). Этот метод позволяет создать из набора записей переменную VBA - двумерный массив, состоящий из строк и столбцов набора. Первый параметр указывает число строк, переписываемых из набора в массив. Значение по умолчанию этого параметра указывает, что переписываются все строки до конца набора. Если Провайдер поддерживает закладки, то можно задать закладку, как значение параметра Start, определяющую строку, начиная с которой набор будет переписываться в массив. Можно в массив записать выборочные поля набора записей, задав параметр Fields соответствующим образом - в виде массива имен или порядковых номеров переписываемых полей.
Function GetString([StringFormat As StringFormatEnum = adClipString], [NumRows As Long = -1], [ColumnDelimeter As String], [RowDelimeter As String], [NullExpr As String]) As String. Если предыдущий метод позволяет преобразовать набор записей в двумерный массив, то метод GetString позволяет конвертировать набор записей в строку. Не буду останавливаться на деталях такого преобразования
Sub Move(NumRecords As Long, [Start]), Sub MoveFirst(), Sub MoveLast(),Sub MoveNext(),Sub MovePrevious(). Эта группа методов позволяет организовать перемещение по записям набора. Методы неоднократно появлялись в наших примерах, и я полагаю, семантика их не нуждается в дополнительных пояснениях.
Function NextRecordset([RecordsAffected]) As Recordset. Метод Execute объекта Command или метод Open объекта Recordset может выполнять составные команды, возвращающие множество наборов данных. Например, командой может быть составной оператор SQL вида: "Select * From [Книги]; Select * From [Заказчики]". В таких случаях необходимо вызвать метод NextRecordset для получения очередного набора записей. Если множество наборов исчерпано, то метод возвращает значение Nothing. Синтаксически очередной объект может связываться с той же объектной переменной, если старый набор уже не нужен.
Sub Requery([Options As Long = -1]). Вызов этого метода эквивалентен последовательности вызовов методов Open и Close. Он позволяет обновить состояние набора, синхронизируя его с текущим состоянием базы данных. Параметр Options определяет специфику выполнения обновления, его возможные значения задаются суммой констант из перечислений ExecuteOptionEnum и CommandTypeEnum. Так, например, значение adAsyncExecute указывает, что обновление будет идти асинхронно и о его завершении можно будет узнать, когда вызовется обработчик события RecordsetChangeComplete.
Sub Resync([AffectRecords As AffectEnum = adAffectAll], [ResyncValues As ResyncEnum = adResyncAllValues]). Еще один способ синхронизации данных набора записей и базы данных. В отличие от выше описанного метода Requery запрос повторно не выполняется, - восстанавливается значения только тех записей, которые есть в наборе. Поэтому, если запись набора будет удалена другим пользователем, то возникнет ошибка, информация о которой сохранится в коллекции Errors. Метод полезен при работе со статическим курсором или курсором типа Forward Only.
Sub Save([Destination], [PersistFormat As PersistFormatEnum = adPersistADTG]). Позволяет сохранить набор записей в файле или объекте Stream. Параметр Destination задает полный путь к файлу, в котором должен быть сохранен объект Recordset или ссылку на объект Stream. Второй параметр задает формат сохранения - XML или ADTG.
Sub Seek(KeyValues, [SeekOption As SeekEnum = adSeekFirstEQ]). Еще один, наряду с методом Find, метод поиска записи в наборе. Напомню, поля в таблицах базы данных могут быть индексированными. Индексы позволяют организовать быстрый поиск нужной записи. Метод Seek позволяет по ключевым значениям индексируемых полей найти запись в наборе и сделать ее текущей. В его первом параметре KeyValues задается массив значений полей, составляющих индекс. Параметр SeekOption задает тип сравнения между столбцами индекса и соответствующими значениями KeyValues.
Метод используется в сочетании со свойством Index, которое задает соответствующий индекс.
Поскольку не все Провайдеры поддерживают работу с индексами, то перед вызовом метода полезно, как обычно, вызвать метод Supports с константой adSeek или adIndex, чтобы выяснить возможность такого вызова в используемом контексте. Провайдер базы данных Access не поддерживает работу с индексами и для него единственным методом поиска является метод Find.
Function Supports(CursorOptions As CursorOptionEnum) As Boolean. Метод, позволяющий выяснить возможности Провайдера в данном контексте. Параметр CursorOptions задает одно из возможных свойств курсора, метод возвращает значение True, если Провайдер поддерживает это свойство и False в противном случае. Возможные значения параметра являются константами из перечисления CursorOptionEnum. Ряд констант были упомянуты при описании свойств и методов объекта Recordset.
Sub Update([Fields], [Values]), Sub UpdateBatch([AffectRecords As AffectEnum = adAffectAll]). Два важных метода, позволяющих проводить обновление отдельной записи или пакета записей в базе данных. Примеры применения метода Update уже приводились.
О курсоре
Прежде чем продолжить рассмотрение свойств, есть смысл посвятить отдельный параграф курсору - важному понятию в модели ADO и при работе с базами данных. Курсор - это элемент базы данных, позволяющий управлять перемещением по записям, обновлением данных, видимостью изменений, сделанных другими пользователями. В реляционных базах данных в результате запроса возвращается набор строк таблицы (записей). Приложению, работающему с этим набором в каждый текущий момент необходим не весь набор, а отдельная запись или небольшой блок из записей набора. Для работы с набором записей приложению необходим программный механизм, который будет управлять позициями записей в наборе при его изменении, скроллингом - перемещением вперед и назад по записям набора, разрешением конфликтов при одновременном доступе многих пользователей к одной и той же записи. Все эти службы и предоставляются совокупностью программных компонент, называемых курсорами. Они реализованы в виде библиотеки курсоров, являющейся, обычно, частью базы данных. Название "Курсор" связано с тем, что, так или иначе, курсор указывает на текущую запись в наборе. Поскольку от решения вопросов, относящихся к курсорам, зависит эффективность работы с базой данных, и эти решения могут играть определяющую роль, то следует уметь правильно выбирать тип курсора, положение курсора и другие его характеристики. Тип курсора задает, как будет идти скроллинг, динамику изменения набора записей и многое другое. Положение курсора определяет, где будет идти основная работа с ним - на клиентской или серверной стороне. Рассмотрим возможные типы курсоров. Их четыре:
Forward Only - допускает перемещение по записям только вперед, не допуская полноценный скроллинг. Он обычно используется тогда, когда необходим только один проход по записям. Его можно использовать и в случаях нескольких проходов, закрывая и заново открывая курсор. После обработки очередной строки освобождаются ресурсы, связанные с ее хранением. По умолчанию этот курсор является динамическим, что означает, что он следит за всеми изменениями, сделанными другими пользователями в показываемых записях. Конечно, это не относится к уже просмотренным записям. Константа adOpenForwardOnly из перечисления CursorTypeEnum задает этот тип курсора.
Static - курсор этого типа в отличие от динамического не следит за изменениями, сделанными другими пользователями. Он сохраняет состояние набора записей на момент открытия. В зависимости от реализации статические курсоры могут допускать только чтение записей или возможность их обновления, могут допускать скроллинг или только перемещение вперед. Как правило, статический курсор допускает видимость изменений, сделанных самим приложением. Этот вид курсора обычно применяется, когда необходим скроллинг, но нет необходимости следить за изменениями, сделанными другими пользователями. Константа adOpenStatic задает этот тип курсора.
Keyset - курсор этого типа является, обычно, альтернативой курсору Forward Only. Он применяется, когда идет интенсивная работа с отдельными записями набора в произвольном порядке доступа к ним. Этот курсор называется курсором, управляемым набором ключей (keyset driven cursor). Для каждой строки из набора создается ключ, обеспечивающий быстрый доступ к любой строке набора.
Что касается наблюдения за изменениями в наборе, то этот вид курсора обеспечивает стратегию, промежуточную между статическим и динамическим курсором. Он позволяет проследить за изменениями значений записей, сделанных другими пользователями, и этим он похож не динамический курсор. Но он не позволяет проследить за изменениями в составе набора - добавлению или удалению строк, изменения порядка их следования. При создании ключей состав набора замораживается. Когда запись удаляется из набора, то, поскольку ключ для нее сохраняется, то такая запись будет видна, как пустая запись - "дыра" в наборе. Добавляемые записи будут видны в виде добавлений в конец набора. Константа adOpenKeyset задает этот тип курсора.
Dynamic - динамический курсор обнаруживает все изменения, происходящие с набором записей, сделанные как самим приложением, так и всеми параллельно работающими пользователями. Все вставки, обновления или удаления, сделанные всеми пользователями видимы для этого типа курсора. Этот вид курсора выбирается, когда необходимо обеспечить совместную работу, но, нужно понимать, он требует от сервера больших затрат и при большом числе пользователей может существенно замедлить работу с набором данных. Константа adOpenDynamic задает этот тип курсора.
Поговорим теперь о такой важной характеристике курсора как его положение (Cursor Location). Константы adUseClient и adUseServer перечисления CursorLocationEnum задают положение курсора. В зависимости от установленного значения все необходимые ресурсы и сама работа с данными ведется на клиентской или серверной стороне. Достоинства работы на стороне клиента состоят в быстром отклике на обращение к записям, не требующим выхода в сеть. Когда работа с записями ведется в основном в режиме их чтения, то это положение курсора наиболее предпочтительно. Некоторым недостатком является то, что при больших наборах клиентскому компьютеру требуются большие ресурсы по памяти для хранения данных. Преимущества работы также теряются, когда интенсивно изменяется состав набора - записи удаляются, добавляются, так как эти изменения должны отражаться и на сервере. Если курсор расположен на серверной стороне, то объем передаваемых данных может быть существенно уменьшен, поскольку вся обра ботка ведется на сервере и клиенту переда ется только необходимые ему записи, а не весь набор. С другой стороны при большом числе активных пользователей каждому из них сервер должен выделить ресурсы, что может быть серьезной нагрузкой на сервер, с которой он может и не справиться. Еще одним недостатков курсора на серверной стороне является то, что в отличие от курсоров на клиентской стороне, не поддерживается работа в пакетном режиме (batch cursor), а возможна работа только с единственной записью.
Конечно, поддерживают работу на клиентской и серверной стороне разные библиотеки курсоров. При работе на стороне клиента для обеспечения работы с курсором ADO вызывает специальную службу - Microsoft Cursor Service for OLE DB, поддерживающую единую функциональность для различных Провайдеров.
Для того чтобы обеспечить целостность данных в СУБД имеются специальные механизмы, позволяющие временно закрыть доступ к записям базы другим пользователям, пока один из пользователей корректирует их содержание. С одной стороны, возможность закрытия доступа совершенно необходима в некоторых ситуациях, например, чтобы не продать один и тот же билет на самолет или поезд разным пассажирам. С другой стороны, когда, например, в интернете сотни тысяч пользователей обращаются одновременно к базе данных, то закрытие данных существенно снижает общую производительность системы. Система ADO позволяет указывать Провайдеру, какой тип закрытия данных следует применять при работе с объектами Recordset. Конкретный Провайдер может не поддерживать все типы закрытия, в этом случае он будет поддерживать возможный ближайший тип закрытия. Вернемся теперь к рассмотрению свойств объекта Recordset и продолжим это рассмотрение со свойств, связанных с курсором.
Объект Command
Объект Connection позволяет установить соединение с Провайдером источника данных и, вообще говоря, выполнять операции над данными. Но для выполнения команд есть специальный объект. Объект Command позволяет задать команду Провайдеру на выполнение той или иной операции над данными источника. Чтобы разобраться с возможностями этого объекта, давайте начнем с рассмотрения его свойств и методов. Замечу, что этот объект событий не имеет, - они связаны только с объектами Connection и Recordset.
Объект Connection
С чего начинается работа с источником данных? Прежде всего, нужно с ним соединиться. Объект Connection, как я уже говорил, позволяет установить соединение с Провайдером источника данных.
Объект Recordset
Этот объект представляет набор записей, возвращаемый в результате выполнения операции. В каждый момент можно работать только с одной выделенной записью набора, которая называется текущей записью. Естественно, есть методы, позволяющие перемещаться по записям набора. В предыдущем примере при работе с набором записей для этой цели использовались методы MoveFirst и MoveLast. При использовании ADO практически вся работа с данными ведется через этот объект. Он используется для того, чтобы читать записи из базы данных, изменять их содержимое, удалять и добавлять новые записи. Это самый сложный по своей организации объект ADO, - у него больше всего свойств, методов и событий. В предыдущем примере уже демонстрировалось применение этого объекта, где он создавался в результате выполнения команды, получающей результаты запроса к базе данных, затем полученные данные использовались для просмотра. Прежде чем обсудить все те возможности, которые предоставляет этот объект, давайте, как обычно, вначале рассмотрим его свойства, методы и события.
Объектная модель ADO
Рассмотрим объектную модель ADO и начнем с графического представления отношений между объектами в этой модели:
Рис. 5.1. Отношения между объектами в объектной модели ADO Объекты ADO имеют следующее назначение:
Command - Определяет ту специальную команду, которую предполагается выполнить над источником данных.
Connection - Задает открытое соединение с источником данных.
Error - элемент коллекции Errors. Содержит описание ошибки доступа, которая возникла при выполнении Провайдером определенной операции. Список этих ошибок (коллекция Errors), возникших в течение одной операции создается Провайдером данных.
Field - элемент коллекции Fields. Поле записи, задает столбец данных, все значения в котором имеют один и тот же тип.
Parameter - элемент коллекции Parameters. Представляет параметр или аргумент, связанный с объектом Command, который определяет параметризованный запрос или хранимую процедуру. Параметры передаются запросу или процедуре.
Property - элемент коллекции Properties. Представляет характеристики объектов ADO, используемые Провайдером.
Record - Представляет единственную запись - строку в наборе записей - объекте Recordset, или каталог или файл в файловой системе.
Recordset - Представляет все множество записей таблицы базы данных или результат выполнения некоторой команды. В каждый текущий момент объект Recordset ссылается только на одну запись, называемую текущей, внутри всего набора записей.
Stream - Представляет бинарный поток данных, рассматриваемый как последовательность байтов.
Хочу обратить внимание на некоторую особенность данной объектной модели. На верхнем уровне иерархии находится целая группа объектов. Здесь нет центрального объекта, как это обычно бывает, в который вложены все остальные объекты. Замечу, что в предыдущей модели DAO такой объект был - это объект DBEngine, задающий некую машину базы данных. В данном случае Microsoft отошла от привычной для Office 2000 практики и отказалась от введения центрального объекта, в который вложены все остальные объекты иерархии. Чтобы дать полную характеристику объектов, нужно рассмотреть их свойства, методы и события. Кроме того, нужно понимать отношения, связывающие объекты, а еще хорошо бы понимать, как пользоваться всем этим богатством. Поговорим об этом.
Обзор возможностей объекта Command и примеры применения
Подведу теперь некоторые итоги и укажу на те возможности, которые предоставляет объекта Command:
Задать описание команды, используя свойства CommandText и CommandStream. Второе из этих свойств позволяет, например, задавать XML-запросы.
Связать объект Command с открытым соединением, используя свойство ActiveConnection.
Вызывать команду на исполнение, используя метод Execute объекта Command, или для именованного объекта вызывать команду как метод объекта Connection.
Формировать объект Recordset как результат выполнения команды.
В момент вызова команды передавать параметры хранимой процедуре или параметризованному запросу.
Формировать при необходимости объекты Parameter и создавать коллекцию Parameters.
Повышать эффективность выполнения команды, используя свойства Prepared и CommandType.
Приведу пример создания и работы с объектом Command: Public Sub CreateCommands() 'Создание команд, выполняющих операции 'с тестовой базой данных Access Dim Par1 As Object Dim strSQL1 As String, strSQL2 As String Dim strSQL3 As String, strSQL4 As String Dim KeyAuthor As String, KeyName As String Const Кавычка = "'" KeyAuthor = "Б. Гейтс": KeyName = "Дорога в будущее" 'Задание четырех SQL операторов strSQL1 = "Select * FROM [Книги]WHERE [Автор]= " _ & Кавычка & KeyAuthor & Кавычка strSQL2 = "Select * FROM [Книги]WHERE ([Название]=" _ & Кавычка & KeyName & Кавычка & " AND [Автор] = " _ & Кавычка & KeyAuthor & Кавычка & ")" strSQL3 = "Select * FROM [Заказчики в Твери]" strSQL4 = "Select * FROM [зак-из-гор]"
'задание свойств объекта Command Cmd1.ActiveConnection = Con1 Cmd1.CommandText = strSQL1 Cmd1.CommandType = adCmdText Cmd1.Prepared = True 'Cmd1.Name = "AuthorAndBook"
'вызов команды на исполнение методом Execute Set Rst1 = Cmd1.Execute 'печать результата Rst1.MoveFirst Debug.Print Rst1!Автор Debug.Print Rst1!Название Debug.Print Rst1!Цена Rst1.MoveLast Debug.Print Rst1!Автор Debug.Print Rst1!Название Debug.Print Rst1!Цена
'Изменение описания команды Cmd1.CommandText = strSQL2 Set Rst1 = Cmd1.Execute 'печать результата Debug.Print Rst1!Автор Debug.Print Rst1!Название Debug.Print Rst1!Цена
' Вызов хранимого запроса базы данных Cmd1.CommandText = strSQL3 Set Rst1 = Cmd1.Execute 'печать результата Rst1.MoveFirst Debug.Print Rst1!Название Rst1.MoveLast Debug.Print Rst1!Название
Я выполняю четыре разные команды над базой данных. Описание этих команд я задал в четырех SQL-операторах. Первый из них выбирает из таблицы "Книги" все книги, автор которых задан переменной KeyAuthor, имеющей в данном примере значение "Б. Гейтс". Замечу, что в таблице хранятся сведения о двух книгах этого автора. Во втором операторе запрос уточняется и ищется книга с фиксированным названием. Третий SQL-оператор обращается к стандартному запросу, хранимому в базе данных. Этот запрос выдает в качестве результата набор записей, содержащих заказчиков из города Тверь. Наконец, текст четвертой команды представляет вызов параметризованного запроса, где ищутся заказчики из города, название которого является параметром запроса.
Прежде чем вызвать первую команду на исполнение, я формирую свойства объекта Command. Первым делом формирую свойство CommandText, задающее описание команды, но не только его. Я задал свойство ActiveConnection, что совершенно необходимо, а, кроме того, задал свойства Prepared и CommandType. Заметьте, в данном контексте невозможно именовать команду, поэтому мне пришлось закомментировать назначение этого свойства, чтобы избежать появления ошибки.
Последовательно изменяя описание команды - свойство CommandText, - я выполнил все четыре команды. Конечно же, прежде чем выполнить четвертую команду, требующую задания параметра в момент ее выполнения, мне пришлось этот параметр создать. Для его создания я вызвал метод CreateParameter и созданный таким образом параметр присоединил к коллекции Parameters. Хочу обратить внимание на одну деталь, переменную Par1 мне пришлось описать как имеющую класс Object, а не класс Parameter, иначе возникало несоответствие типов при попытке присвоения ей ссылки на объект, созданный в результате выполнения метода CreateParameter.
Для демонстрации корректности выполнения команд, я печатаю значения записей из набора Rst1, создаваемого при выполнении очередной команды. Заметьте, я использую некоторые операции над объектом Recordset - MoveFirst и MoveLast, позволяющие выбрать нужную запись в наборе. Чуть позже я подробно рассмотрю все свойства и методы этого объекта.
В заключение, я печатаю некоторые свойства объекта Command. Из большой коллекции Properties я ограничился печатью количества элементов в этой коллекции, а также имени и значения первого элемента в этой коллекции. Приведу результаты отладочной печати:
Б. Гейтс Дорога в будущее 23 Б. Гейтс Бизнес со скоростью мысли 150 Б. Гейтс Дорога в будущее 23 Книжная лавка ООО 'Тверькнига' Книжная лавка ООО 'Тверькнига' ActiveConnection = Provider=Microsoft.Jet.OLEDB.4.0;Password="";User ID=Admin;Data Source=c:\!O2000\DsCd\Ch14\dbPP2000.mdb;Mode=Share Deny None;Extended Properties="";Jet OLEDB:System database="";Jet OLEDB:Registry Path="";Jet OLEDB:Database Password="";Jet OLEDB:Engine Type=5;Jet OLEDB:Database Locking Mode=1;Jet OLEDB:Global Partial Bulk Ops=2;Jet OLEDB:Global Bulk Transactions=1;Jet OLEDB:New Database Password="";Jet OLEDB:Create System Database=False;Jet OLEDB:Encrypt Database=False;Jet OLEDB:Don't Copy Locale on Compact=False;Jet OLEDB:Compact Without Replica Repair=False;Jet OLEDB:SFP=False CommandTimeout = 30 CommandType = 1 Name = CommandText = Select * FROM [зак-из-гор] Prepared = True Parameters.Count = 2 Properies.Count = 88 State = 0 Properties(1).Name = Blocking Storage Objects Properties(1).Value = True
'Изменение описания команды Cmd1.CommandText = strSQL2 Set Rst1 = Cmd1.Execute 'печать результата Debug.Print Rst1!Автор Debug.Print Rst1!Название Debug.Print Rst1!Цена
' Вызов хранимого запроса базы данных Cmd1.CommandText = strSQL3 Set Rst1 = Cmd1.Execute 'печать результата Rst1.MoveFirst Debug.Print Rst1!Название Rst1.MoveLast Debug.Print Rst1!Название
Я выполняю четыре разные команды над базой данных. Описание этих команд я задал в четырех SQL-операторах. Первый из них выбирает из таблицы "Книги" все книги, автор которых задан переменной KeyAuthor, имеющей в данном примере значение "Б. Гейтс". Замечу, что в таблице хранятся сведения о двух книгах этого автора. Во втором операторе запрос уточняется и ищется книга с фиксированным названием. Третий SQL-оператор обращается к стандартному запросу, хранимому в базе данных. Этот запрос выдает в качестве результата набор записей, содержащих заказчиков из города Тверь. Наконец, текст четвертой команды представляет вызов параметризованного запроса, где ищутся заказчики из города, название которого является параметром запроса.
Прежде чем вызвать первую команду на исполнение, я формирую свойства объекта Command. Первым делом формирую свойство CommandText, задающее описание команды, но не только его. Я задал свойство ActiveConnection, что совершенно необходимо, а, кроме того, задал свойства Prepared и CommandType. Заметьте, в данном контексте невозможно именовать команду, поэтому мне пришлось закомментировать назначение этого свойства, чтобы избежать появления ошибки.
Последовательно изменяя описание команды - свойство CommandText, - я выполнил все четыре команды. Конечно же, прежде чем выполнить четвертую команду, требующую задания параметра в момент ее выполнения, мне пришлось этот параметр создать. Для его создания я вызвал метод CreateParameter и созданный таким образом параметр присоединил к коллекции Parameters. Хочу обратить внимание на одну деталь, переменную Par1 мне пришлось описать как имеющую класс Object, а не класс Parameter, иначе возникало несоответствие типов при попытке присвоения ей ссылки на объект, созданный в результате выполнения метода CreateParameter.
Для демонстрации корректности выполнения команд, я печатаю значения записей из набора Rst1, создаваемого при выполнении очередной команды. Заметьте, я использую некоторые операции над объектом Recordset - MoveFirst и MoveLast, позволяющие выбрать нужную запись в наборе. Чуть позже я подробно рассмотрю все свойства и методы этого объекта.
В заключение, я печатаю некоторые свойства объекта Command. Из большой коллекции Properties я ограничился печатью количества элементов в этой коллекции, а также имени и значения первого элемента в этой коллекции. Приведу результаты отладочной печати:
Б. Гейтс Дорога в будущее 23 Б. Гейтс Бизнес со скоростью мысли 150 Б. Гейтс Дорога в будущее 23 Книжная лавка ООО 'Тверькнига' Книжная лавка ООО 'Тверькнига' ActiveConnection = Provider=Microsoft.Jet.OLEDB.4.0;Password="";User ID=Admin;Data Source=c:\!O2000\DsCd\Ch14\dbPP2000.mdb;Mode=Share Deny None;Extended Properties="";Jet OLEDB:System database="";Jet OLEDB:Registry Path="";Jet OLEDB:Database Password="";Jet OLEDB:Engine Type=5;Jet OLEDB:Database Locking Mode=1;Jet OLEDB:Global Partial Bulk Ops=2;Jet OLEDB:Global Bulk Transactions=1;Jet OLEDB:New Database Password="";Jet OLEDB:Create System Database=False;Jet OLEDB:Encrypt Database=False;Jet OLEDB:Don't Copy Locale on Compact=False;Jet OLEDB:Compact Without Replica Repair=False;Jet OLEDB:SFP=False CommandTimeout = 30 CommandType = 1 Name = CommandText = Select * FROM [зак-из-гор] Prepared = True Parameters.Count = 2 Properies.Count = 88 State = 0 Properties(1).Name = Blocking Storage Objects Properties(1).Value = True
Заметьте, число параметров в коллекции Parameters равно 2, а не 1, как должно было бы быть. Это связано с моей недоработкой и недоработкой Microsoft. Перед тем, как начать формировать коллекцию Parameters, я должен был очистить ее содержимое, вызвав метод Delete. Я не сделал этого, поскольку метод Delete не вызывается в данном контексте. По этой причине при повторном запуске процедуры произошло добавление параметра к уже имеющейся коллекции. В данном случае, когда выполняется запрос с одним параметром, это не приводит к ошибке, и я не стал усложнять уже и так довольно длинную процедуру. Но в принципе это серьезная ошибка, которая в другой ситуации может привести к неприятностям, например, если бы в следующей команде я попытался бы выполнить другой параметризованный запрос. Так что обратите внимание на эту ситуацию, и корректно работайте с коллекцией Parameters. Заметьте, проблемы исчезают для локально определенного объекта Command .
Обзор возможностей объекта Connection
Подведу теперь некоторые итоги и укажу на некоторые дополнительные возможности объекта Connection. Объект задает сеанс работы с источником данных. В случае, когда речь идет об удаленном источнике и клиент-серверном приложении установление соединения означает физическое подключение к серверу в сети. Хотя большинство свойств и методов этого объекта определено для всех стандартных Провайдеров, тем не менее, есть и специфика, определяемая каждым конкретным Провайдером. Перечислю еще раз те возможности, которые предоставляют свойства и методы объекта:
Еще до открытия соединения можно установить его конфигурацию, задавая характеристики соединения с помощью таких свойств, как ConnectionString, ConnectionTimeOut, Mode. Первое из этих свойств имеет статус свойства по умолчанию.
Можно обращаться на клиентской стороне к службе управления курсором - Cursor Service for OLE DB, задав adUseClient в качестве значения свойства CursorLocation.
Можно установить для соединения базу данных по умолчанию, задав свойство DefaultDatabase, аналогично, свойство Provider устанавливает Провайдера по умолчанию.
Используя методы Open и Close, можно многократно открывать и закрывать соединение, возможно, меняя его параметры от сеанса к сеансу.
Используя метод Execute, можно выполнять те или иные команды, не обращаясь к объекту Command.
Дана возможность управлять транзакциями в открытом соединении, в том числе гнездованными транзакциями, если только Провайдер поддерживает эти возможности.
В соответствующих обработчиках событий можно проверять корректность задания параметров перед выполнением команд. После их выполнения можно анализировать объекты Error, чтобы понять причины, приведшие к возникновению ошибки по завершении команды.
Можно независимо создать несколько объектов Connection, что позволяет запускать одновременно несколько сеансов работы с одним или разными источниками данных.
Наконец, важно отметить такую возможность некоторых Провайдеров, как вызов объектом Connection именованных команд и хранимых процедур, точно также как, если бы они были методами этого объекта. Вот схема процедуры, демонстрирующая эту возможность:
Sub Fragment() 'Это не исполняемый код, а демонстрационная заготовка Dim cnn As New ADODB.Connection Dim cmd As New ADODB.Command Dim rst As New ADODB.Recordset 'конфигурирование соединения cnn.Open ConnectionString:= "…" cmd.Name = "NameOfMyCommand" cmd.ActiveConnection = cnn 'запуск команды на выполнение, передавая ей параметры и объект Recordset cnn.NameOfMyCommand "parameter", rst 'запуск хранимой процедуры и передача ей параметров cnn.NameOfMyStoredProcedure "parameter" End Sub
Приведенный пример является лишь иллюстрацией, чтобы он заработал необходимо уточнить опущенные здесь детали. Такие примеры тоже полезны, но, пожалуй, интереснее работающие процедуры. Чтобы проводить эксперименты с объектами ADO, я создал некоторый модуль в программном проекте документа. Приведу сейчас процедуру этого модуля, в которой создается соединение.
'Модуль TestingADO 'Глобальные переменные Public Con1 As New ADODB.Connection Public Cmd1 As New ADODB.Command Public Rst1 As New ADODB.Recordset Public Strm1 As New ADODB.Stream Public Sub CreateConnection() 'Создание соединения с тестовой базой данных Access Dim strConnStr As String If Con1.State = adStateOpen Then Con1.Close 'закрыть соединение 'Вариант 1 'Конфигурирование соединения Con1 Con1.Provider = "Microsoft.jet.oledb.4.0" Con1.ConnectionString = "Data Source=c:\!O2000\DsCd\Ch14\dbPP2000.mdb" Con1.CursorLocation = adUseClient 'Открытие соединения Con1.Open 'Вариант 2 'strConnStr = "Provider=Microsoft.jet.oledb.4.0;" & _ '"Data Source=c:\!O2000\DsCd\Ch14\dbPP2000.mdb" 'Con1.Open strConnStr 'печать характеристик соединения Debug.Print "Attributes = ", Con1.Attributes Debug.Print "CommandTimeout = ", Con1.CommandTimeout Debug.Print "ConnectionString = ", Con1.ConnectionString Debug.Print "ConnectionTimeout = ", Con1.ConnectionTimeout Debug.Print "CursorLocation = ", Con1.CursorLocation Debug.Print "DefaultDatabase = ", Con1.DefaultDatabase Debug.Print "Mode = ", Con1.Mode Debug.Print "Properies.Count = ", Con1.Properties.Count Debug.Print "State = ", Con1.State Debug.Print "Version = ", Con1.Version End Sub
Приведу краткий комментарий.
Прежде всего, отмечу, что я начал работать с документом Excel, который будет получать данные из базы данных Access, создание которой я описал в предыдущей главе. В проекте этого документа я создал модуль с именем TestingADO, подключил к проекту библиотеку ActiveX Data Objects 2.5 Library, которая у меня на компьютере находится, как обычно, по адресу: "c:\Program Files\Common Files\System\ado\msado15.dll". Имя этой библиотеки, используемое в проектах на VBA, - ADODB.
Затем я создал 4 глобальных объекта: Con1, Cmd1, Rst1, Strm1, принадлежащих соответственно классам Connection, Command, Recordset, Stream из библиотеки ADODB.
Первая процедура, которую я написал в этом модуле - CreateConnection, проста. В ней идет работа с объектом Con1, устанавливается соединение с базой данных Access и распечатываются характеристики сделанного соединения.
Работа начинается с проверки состояния соединения, поскольку попытка открыть уже открытое соединение приводит к ошибке.
Я рассматриваю два варианта установления соединения. В первом - предварительно задаются свойства соединения Provider и ConnectionString, затем вызывается метод Open без параметров. Во втором варианте вся необходимая информация указывается при вызове метода Open в передаваемых ему параметрах.
Приведу результаты печати свойств соединения после его открытия:
Обратите внимание на длинную строку ConnectionString. Она, конечно, намного длиннее той строки, которую задал я, определив источник данных. Помимо этого, строка содержит многие характеристики соединения, устанавливаемые по умолчанию. Заметьте также, я напечатал лишь число элементов коллекции Properties, не приводя значений всех 94 элементов.
Обзор возможностей объекта Recordset
Объект Recordset это основной объект, позволяющий работать с данными, извлеченными и помещаемыми в базу данных. Создается разными способами - методом Execute объектов Connection и Command, либо методом Open объекта Recordset. Содержит набор записей, полученных в результате выполнения запроса. В каждый текущий момент позволяет работать только с одной записью набора - текущей, на которую указывает курсор. Методы и свойства объекта позволяют перемещать курсор по набору записей, производить фильтрацию записей, их сортировку, находить нужную запись. Можно изменять значения отдельных полей некоторых записей, добавлять новые и удалять записи из набора. Измененный набор можно сохранить в базе данных. При этом изменения можно делать оперативно - для каждой записи, подвергшейся изменению, или в пакетном режиме. Во многом возможность выполнения операций над данными определяет тип и положение курсора, установленные свойствами объекта Recordset. В ADO определены четыре типа курсора и два возможных положения, указывающие, где выполняются операции - на клиентской или на серверной стороне. Свою специфику, определяющую возможность выполнения отдельных операций и их особенность, накладывает используемый Провайдер. Свойство Supports позволяет выяснить, поддерживает ли Провайдер те или иные свойства выбранного курсора. Свойство Fields является свойством по умолчанию объекта Recordset. Оно возвращает коллекцию полей, элементы которой являются объектами Field, каждый из которых позволяет работать с отдельным полем записи. Объект Recordset может быть не только плоским набором записей, но и иметь иерархическую структуру, при которой записи набора могут иметь потомков. На этом я завершу описание основных объектов ADO - Connection, Command и Recordset. Но разговор об объектах ADO еще далеко не завершен и будет продолжен в следующей главе.
Семейство объектов ADO
Перейдем теперь к подробному рассмотрению объектов ADO - наиболее интересных для VBA-программистов. Более точно, следует говорить о семействе объектов ADO, в которое входят три группы объектов:
ADO - ActiveX Data Objects. Иногда, когда говорят об объектах ADO, имеют в виду именно объекты этой группы. Эти объекты, как я уже говорил, являются надстройкой над объектами OLE DB. Их назначение - решать те же задачи, ради которых был создан интерфейс OLE DB, но на более высоком языковом уровне общения. В первую очередь эти объекты создавались для того, чтобы дать возможность создавать клиент-серверные и WEB-ориентированные приложения на VB/VBA и VBScript. Вот три главных объекта, находящихся на верхнем уровне иерархии этой группы объектов:
Connection - позволяет установить соединение с Провайдером.
Command - позволяет задать команду Провайдеру, определяющую операцию, выполняемую над данными.
Recordset - сохраняет результаты выполненной команды и обеспечивает, тем самым, возможность работы с результатами запросов и хранимых процедур.
ADOX - ActiveX Data Objects Extensions for Data Definition Language and Security. Объекты этой группы дополняют функциональные возможности объектов первой группы. Они позволяют манипулировать не с данными, а с объектами более высокого уровня - схемами данных. С их помощью можно модифицировать схему данных, создавать, изменять и удалять ее объекты, например таблицы базы данных. Другое назначение этих объектов - управлять безопасностью доступа к данным, - назначать и изменять права доступа к данным, создавать группы пользователей, обладающих определенными правами. Центральными объектами в этой группе, находящимися на верхнем уровне иерархии, являются объекты Catalog, User, Group. Первый из этих объектов задает источник данных, - он соответствует объекту DataBase в модели объектов DAO. Объекты User и Group позволяют управлять безопасностью доступа, как отдельного пользователя, так и пользователей, объединенных в группы.
ADO MD - ActiveX Data Objects MultiDimensional. Также как и обычные ADO-объекты, эти объекты являются надстройкой над интерфейсом OLE DB. Их назначение состоит в том, чтобы обеспечить возможность работы с многомерными источниками данных. Для того, чтобы работа с этим объектами была возможной, соответствующий Провайдер должен быть Провайдером многомерных данных (MDP Provider) и удовлетворять специальным спецификациям - OLE DB for OLAP. В этом случае данные представляются не в виде таблиц, а в виде кубов данных. Центральными в этой группе являются объекты CubeDef и CellSet, которые обеспечивают определение куба данных и доступ к множеству ячеек этого куба.
Каждая из трех групп ADO-объектов находится в отдельной DLL библиотеке и подключается к программному проекту независимо друг от друга обычным способом - вручную через меню References, либо программно. Замечу, что описание объектов, которое я буду приводить, соответствует версии библиотеки 2.6, - последней на момент написания данного текста, хотя примеры, которые я буду приводить, разработаны на предыдущей версии 2.5.
События объекта Connection
Только два объекта ADO обладают событиями - Connection и Recordset. События связываются с выполнением той или иной операции и могут возникать перед выполнением и после завершения операции. При возникновении события операционной системе посылается уведомление, а она, в свою очередь, вызывает соответствующую процедуру обработки этого события. Конечно, программист должен определить эту процедуру в соответствующем месте проекта. Обработка событий, предшествующих выполнению операции, позволяет проверить правильность задания всех параметров и принять окончательное решение о запуске команды на выполнение или на ее прерывание. Обработка событий после завершения операции особенно важна при асинхронном способе выполнения команд, когда выполнение операций над данными выполняется параллельно с выполнением кода программного проекта. В этом случае необходимо получать уведомление о завершении очередной команды и выполнять в обработчике события определенные действия. У объекта Connection достаточно много событий - 9, большая часть из которых возникает по завершении той или иной команды. Давайте рассмотрим эти события:
Event WillConnect(ConnectionString As String, UserID As String, Password As String, Options As Long, adStatus As EventStatusEnum, pConnection As Connection),
Event WillExecute(Source As String, CursorType As CursorTypeEnum, LockType As LockTypeEnum, Options As Long, adStatus As EventStatusEnum, pCommand As Command, pRecordset As Recordset, pConnection As Connection). Первое из этих событий будет возникать перед выполнением соединения, второе - перед выполнением команды Execute или Open. Параметры pConnection, pCommand, pRecordset возвращают указатели на соответствующие объекты, а остальные параметры задают те или иные характеристики соединения или команды, которую предстоит выполнить. Как я уже говорил, в обработчике события можно проверить корректность задания параметров.
Event BeginTransComplete(TransactionLevel As Long, pError As Error, adStatus As EventStatusEnum, pConnection As Connection),
Event CommitTransComplete(pError As Error, adStatus As EventStatusEnum, pConnection As Connection),
Event RollbackTransComplete(pError As Error, adStatus As EventStatusEnum, pConnection As Connection).
Event ConnectComplete(pError As Error, adStatus As EventStatusEnum, pConnection As Connection),
Event Disconnect(adStatus As EventStatusEnum, pConnection As Connection),
Event ExecuteComplete(RecordsAffected As Long, pError As Error, adStatus As EventStatusEnum, pCommand As Command, pRecordset As Recordset, pConnection As Connection), Шесть событий этой группы возникают после завершения операций Connection, Execute или операций, связанных с транзакциями. Событие ConnectComplete возникает после того, как было открыто соединение, событие Disconnect - после закрытия соединения. Во всех случаях обработчику событий передаются указатели на соответствующие объекты и информация, необходимая, для того чтобы оценить, насколько корректно была выполнена та или иная операция. Если значение параметра adStatus равно adStatusOk, то операция окончилась успешно, и нет необходимости анализировать объект Error. Если же этот параметр имеет значение adStatusErrorsOccured, то необходимо анализировать объект Error, а, возможно, и всю коллекцию Errors.
Event InfoMessage(pError As Error, adStatus As EventStatusEnum, pConnection As Connection). В отличие от остальных это событие встречается не до и не после выполнения операции соединения, а в момент ее выполнения, и выдает предупреждение о возможных ошибках. Параметры, передаваемые обработчику события, имеют привычный смысл.
События объекта Recordset
Только два объекта ADO обладают событиями - Connection и Recordset. События могут возникать перед началом выполнения той или иной команды, что позволяет проверить возможность ее выполнения и произвести отмену выполнения, не дожидаясь появления ошибки. У объекта Recordset таких событий четыре:
Event WillChangeField(cFields As Long, Fields, adStatus As EventStatusEnum, pRecordset As Recordset),
Event WillChangeRecord(adReason As EventReasonEnum, cRecords As Long, adStatus As EventStatusEnum, pRecordset As Recordset),
Event WillChangeRecordset(adReason As EventReasonEnum, adStatus As EventStatusEnum, pRecordset As Recordset),
Event WillMove(adReason As EventReasonEnum, adStatus As EventStatusEnum, pRecordset As Recordset).
Эти события возникают перед тем, как выполняемая операция изменит поле записи, саму запись, например при выполнении операций AddNew или Delete, набор записей, например, при выполнении пакетного обновления, или при перемещении курсора на новую запись. Параметры, передаваемые событию, имеют понятный смысл:
pRecordset - задает указатель на набор записей,
adStatus - определяет состояние, в котором находится операция. Перед началом выполнения операции свойству Status целесообразно присвоить значение adStatusCancel. В обработчике события можно проверить значение этого свойства, если оно имеет значение adStausOk, то операция выполняется нормально.
AdReason - определяет выполняемую операцию, являющуюся причиной появления события.
cFields и Fields - задают соответственно номер поля в массиве Fields, который содержит поля, подлежащие изменениям.
cRecords - номер изменяемой записи.
Большая часть событий связана с окончанием выполнения команды. Вот четыре события, которые дополняют события Will:
Event FieldChangeComplete(cFields As Long, Fields, pError As Error, adStatus As EventStatusEnum, pRecordset As Recordset),
Event RecordChangeComplete(adReason As EventReasonEnum, cRecords As Long, pError As Error, adStatus As EventStatusEnum, pRecordset As Recordset),
Event RecordsetChangeComplete(adReason As EventReasonEnum, pError As Error, adStatus As EventStatusEnum, pRecordset As Recordset),
Event MoveComplete(adReason As EventReasonEnum, pError As Error, adStatus As EventStatusEnum, pRecordset As Recordset).
У всех этих событий помимо уже описанных параметров обработчику события передается еще один параметр pError - указатель на объект Error, позволяющий провести обработку ошибок, если они возникли в ходе выполнения операции. У объекта Recordset есть еще три события:
Event EndOfRecordset(fMoreData As Boolean, adStatus As EventStatusEnum, pRecordset As Recordset). Событие возникает при выполнении операции MoveNext, когда при перемещении по набору достигнут его конец. В обработчике события можно предусмотреть добавление новых записей в конец набора, и повторить выполнение операции. Заметьте, параметр fMoreData следует установить в этом случае как Variant_True.
Event FetchProgress(Progress As Long, MaxProgress As Long, adStatus As EventStatusEnum, pRecordset As Recordset), Event FetchComplete(pError As Error, adStatus As EventStatusEnum, pRecordset As Recordset). Первое из этих событий периодически вызывается, чтобы уведомить сколь много записей было доставлено во время выполнения долгой асинхронной операции. Второе событие возникает по завершении операции доставки записей. Параметр Progress указывает число доставленных записей, а MaxProgress - число ожидаемых для получения записей.
Свойства объекта Command
У объекта Command 9 свойств, из которых 7 - терминальные, а два свойства возвращают коллекции Parameters и Propertiies, что отображено на рис.5.1. Давайте рассмотрим описание всех свойств этого объекта:
Property ActiveConnection As Connection. Заметьте, я был не точен, когда говорил, что только два свойства объекта Command возвращают объекты в качестве результата. Свойство ActiveConnection при открытом соединении также возвращает объект Connection, задающий соединение, связанное с командой. Наличие этого свойства отражает связи между объектами. Нельзя выполнить команду, если не установлено соединение. Свойство позволяет получить доступ к объекту Connection, задающему соединение. Здесь есть, однако, некоторая тонкость, из-за которой это свойство имеет двойственную природу. Дело в том, что при закрытом соединении вернуть ссылку на объект невозможно и потому возвращается строка, содержащая описание соединения.
Свойством ActiveConnection обладают и другие объекты - Recordset и Record.
Заметьте, следует корректно установить значение этого свойства - на открытый объект Connection или на корректно определенную строку, задающую соединение, еще до того, как будет выполняться метод Execute объекта Command, в противном случае возникнет ошибка.
Если между двумя выполнениями команды следует изменить соединение, то вначале необходимо свойству ActiveConnection присвоить значение Nothing, в результате Провайдер освободит ресурсы и корректно произведет операцию отсоединения. После чего можно установить новое соединение. Лишь некоторые Провайдеры позволяют проводить изменения в соединении без промежуточного присваивания значения Nothing.
Изменение соединения оставляет нетронутой коллекцию Parameters, если она заполнялась вручную, значения, устанавливаемые Провайдером, при этом очищаются.
При закрытии соединения, связанного с командой, значение свойства устанавливается в Nothing. Попытка связать команду с закрытым соединением приводит к ошибке.
Property CommandText As String. Устанавливает или возвращает строку, текст которой содержит описание команды для Провайдера. Чаще всего, этот текст содержит описание SQL-оператора, но может быть именем таблицы, относительным URL-адресом или вызовом хранимой процедуры.
Если свойство Prepered установлено как True и объект Command связан с открытым соединением, то Провайдер транслирует текст команды в исполняемый запрос и сохраняет его в таком виде. Это повышает общую эффективность выполнения при многократных вызовах команды.
В зависимости от значения свойства CommandType возможно изменение значения свойства CommandText, чтобы привести его в соответствие со спецификой Провайдера.
Когда необходимо в команде задать ресурс, такой как файл или каталог, то в свойстве CommandText задается URL-адрес ресурса. URL-адрес, использующий http-схему, приводит к автоматическому вызову специального Провайдера - OLE DB Provider for Internet Publishing.
Property CommandStream As Stream. Это свойство появилось в последней на момент написания данного текста версии ADO 2.6. Свойства CommandStream и CommandText является взаимоисключающими. Когда устанавливается свойство CommandStream, то свойство CommandText автоматически становится равным пустой строке. Эти два свойства задают альтернативные способы задания описания команды. Свойство CommandText задает описание строкой. Свойство CommandStream устанавливает или возвращает поток, используемый как ввод для объекта Command. Формат потока определяется спецификой Провайдера.
Property Dialect As String. Свойство определено в версии ADO 2.6. Оно задает диалект - синтаксис и систему правил, которыми руководствуется Провайдер при разборе входной строки или потока, заданного свойствами CommandText и CommandStream. Свойство содержит GUID, который и задает диалект, используемый Провайдером.
Property CommandTimeout As Long. Смысл этого свойства я описывал ранее при рассмотрении объекта Connection. Напомню, значение свойства задает время, в течение которого команда должна быть выполнена. При превышении времени возникнет ошибка.
Property CommandType As CommandTypeEnum. Свойство позволяет установить или определить тип объекта Command, который может быть одним из предустановленных перечислением значений. Тип можно и не устанавливать, - значение свойства по умолчанию - adCmdUnknown. Другие возможные значения типа - adCmdFile, adCmdStoredProc, adCmdTable, adCmdTableDirect, adCmdText. Корректное указание типа повышает эффективность исполнения команды, позволяя Провайдеру не выполнять лишней работы по установлению типа. Вместе с тем неверное указание типа будет приводить к появлению ошибки при вызове метода Execute.
Property Name As String. Возвращает или устанавливает имя команды. Я уже говорил о способе использования этого свойства и демонстрировал схему его применения при описании объекта Connection. Далее я расскажу о том, что мои попытки задать имя команде потерпели неудачу при работе с Провайдером, обеспечивающим работу с базой данных Access.
Property Parameters As Parameters. Является одним из двух свойств-участников объекта Command. В качестве результата возвращает коллекцию параметров, передаваемых Провайдеру, когда команда задает запрос с параметрами или представляет вызов хранимой процедуры, у которой есть параметры. Это свойство является свойством объекта по умолчанию, что, напомню, означает возможность опускать имя свойства при его вызове, так что записи MyCmd.Parameters(i) и MyCmd(i) эквивалентны. Позже я еще буду рассматривать объекты Parameters и Parameter.
Property NamedParameters As Boolean. Это булево свойство определено, начиная с версии ADO 2.6. Когда оно имеет значение True, то имена параметров передаются Провайдеру и могут быть использованы при вызовах команды, требующей передачи параметров. При значении False порядок передачи параметров жестко фиксирован.
Property Prepared As Boolean. Булево свойство, имеет статус "чтение/запись". Я уже говорил, что установка значения True повышает эффективность исполнения команды при многократных вызовах, за счет того, что Провайдер единожды выполняет подготовительную работу и затем хранит скомпилированную версию команды. Не все Провайдеры поддерживают это свойство.
Property Properties As Properties. Еще одно свойство, возвращающее коллекцию объектов. Каждый элемент этой коллекции - объект Property - задает одну из характеристик объекта Command, передаваемых Провайдеру. Этих характеристик, зависящих от Провайдера и объекта довольно много. Вспомните, в предыдущем примере, когда печатались свойства объекта Connection, его коллекция Properties насчитывала 94 элемента.
Property State As Long. Я уже рассказывал об этом свойстве, которым обладают многие объекты ADO, и приводил пример работы с ним.
Свойства объекта Connection
У объекта Connection имеется 13 свойств, два из которых являются свойствами-участниками и возвращают коллекции объектов, остальные являются терминальными свойствами. Свойства, являющиеся объектами, показаны на рис.5.1, где приводится объектная модель ADO. Сейчас же давайте рассмотрим все свойства - терминальные и нетерминальные. Приведу краткое описание свойств:
Property Attributes As Long. Свойство определяет характеристики соединения и может использоваться как для чтения, так и для записи, что позволяет задать или выяснить, каким набором характеристик обладает объект Connection. Значением свойства является сумма значений устанавливаемых характеристик, значение каждой из которой задается соответствующей константой из перечисления XactAttributeEnum. По умолчанию никакие характеристики не задаются и значение этого свойства равно 0. Следует быть осторожным при задании свойства, поскольку значение, представляющее сумму значений несовместимых констант, приводит к возникновению ошибки. Свойством Attributes обладают и другие объекты ADO - Parameter, Field, Property.
Property CommandTimeout As Long, Property ConnectionTimeout As Long. Первым из этих свойств обладают два объекта - Connection и Command. Свойства задают в секундах интервал времени, в течение которого должен выполниться метод Execute, когда он вызывается указанными объектами. Если при выполнении метода произойдет задержка сверх указанного времени, то установление соединения или выполнение команды, заданной методом, будет прервано и возникнет ошибка. По умолчанию значение ConnectionTimeout - максимальное время для установления соединения равно 30 секундам, для выполнения команды - 15 секунд. Оба свойства имеют статус "чтение/запись". Заметьте, что значение первого свойства, установленное для объекта Connection не наследуется объектом Command, связанного с этим соединением. Чтобы корректно установить значения свойств необходимо, конечно представлять каково может быть максимальное время установления соединения и время выполнения той или иной команды.
Property ConnectionString As String. Одно из наиболее важных и постоянно используемых свойств. Свойство имеет статус "чтение/запись". Информация, заданная в тексте строки, используется для установления соединения с источником данных. Синтаксически строка соединения представляет пары вида: аргумент = значение, разделенные символом ";" (точка с запятой). ADO поддерживает пять аргументов, но в зависимости от Провайдера ему могут предаваться и другие аргументы, которые никак не обрабатываются средствами ADO и передаются непосредственно Провайдеру. Вот список общих для всех Провайдеров аргументов, поддерживаемых ADO:
Provider - имя Провайдера, с которым устанавливается соединение.
File Name - имя файла, содержащего предустановленную информацию о соединении, передаваемое провайдеру,.
Remote Provider - имя Провайдера, используемое при открытии соединения на клиентской стороне. Используется только при работе со службой RDS.
Remote Server - имя сервера (путь), используемое при открытии соединения на клиентской стороне. Используется только при работе со службой RDS.
URL - адрес, идентифицирующий такие ресурсы, как файл или каталог. Заметьте, свойства, установленные в строке соединения, могут измениться после открытия соединения, поскольку может произойти трансляция аргументов в форму, предусмотренную Провайдером.
Property CursorLocation As CursorLocationEnum. Свойство позволяет сделать выбор между различными библиотеками курсоров, доступных Провайдеру. Обычно применяемые значения констант - adUseClient и adUseServer позволяют выбрать библиотеку на стороне клиента или сервера. Курсоры, возвращаемые методом Execute, наследуют заданную установку. Также поступают и объекты Recordsets, наследуя установку из связанного с ними соединения.
Property DefaultDatabase As String. Задает имя базы данных по умолчанию. Свойство имеет статус "чтение/запись".
Property Errors As Errors. Свойство имеет статус "только чтение". Возвращает коллекцию объектов Error. Каждый из этих объектов создается автоматически Провайдером, если возникает ошибка при выполнении той или иной команды. Вся коллекция связана только с одной командой. При выполнении новой команды при появлении первой ошибки старая коллекция Errors очищается и начинает создаваться заново. Метод Clear позволяет организовать принудительную чистку коллекции. Заметьте, объекты Error создаются только при выполнении кода Провайдера. Ошибки ADO, возникающие в коде VB/VBA, приводят к появлению события OnError и могут быть обработаны как все динамические ошибки периода выполнения.
Property IsolationLevel As IsolationLevelEnum. Свойство имеет статус "чтение/запись". Специальные константы задают так называемый уровень изоляции. По умолчанию значение константы - adXactChaos. При удаленном доступе на клиентской стороне допустимо только значение - adXactUnspecified.
Property Mode As ConnectModeEnum. Свойство, значения которого задаются перечислением и определяют статус модификации данных. Это свойство имеют объекты Connection, Record и Stream. Для объекта Connection значение по умолчанию - adModeUnknown, для объекта Record - adModeRead, для Stream возможно то или другое значение в зависимости от того, как объект связан с источником данных. Установить значение этого свойства можно только тогда, когда объект закрыт. Изменить статус уже открытого объекта невозможно.
Property Properties As Properties. Второе из свойств объекта Connection, возвращающее в качестве результата объект - коллекцию Properties. Этим же свойством обладают и другие объекты - Command и Recordset. Каждый из объектов Property содержит характеристику, передаваемую Провайдеру.
Property Provider As String. Строка, задающая имя Провайдера. Это имя может быть установлено и другим способом, например, как один из аргументов при задании свойства ConnectionString.
Property State As Long. Многие из объектов ADO обладают этим свойством, которое имеет статус "только для чтения" и определяет совокупность состояний объекта - открыт он или закрыт, а для объектов выполняющих асинхронный метод - состояние выполнения. Поэтому значение свойства задается суммой значений соответствующих констант. Возможные значения констант следующие: adStateClosed = 0, adStateConnecting = 2, adStateExecuting = 4, adStateFetching = 8, adStateOpen = 1. Если свойство возвращает, например, значение 5, то это означает, что объект открыт, а выполняемый метод, например, Execut находится в активном состоянии.
Property Version As String. Свойство имеет статус "только для чтения" и возвращает версию библиотеки ADODB.
Свойства объекта Recordset (продолжение)
Property CursorLocation As CursorLocationEnum, Property CursorType As CursorTypeEnum, Property LockType As LockTypeEnum - группа свойств, связанных с курсором. Общее представление о курсоре уже дано, так что осталось сообщить лишь небольшие детали. Свойство позволяет задать положение курсора. Два его возможных значения: adUseClient и adUseServer определяют, на какой стороне идет работа с курсором - на клиентской или на серверной стороне. Свойство CursorType позволяют определить или задать тип курсора. Я уже определял четыре возможных значения, доступных при задании типа курсора. Свойство LockType позволяет управлять закрытием доступа. На серверной стороне можно задать одно из трех возможных значений этого свойства: adLockReadOnly, adLockOptimistic, adLockPessimistic. На клиентской стороне возможно только одно значение - adLockBatchOptimistic.
Property DataSource As Unknown, Property DataMember As String. Два взаимосвязанных свойства. Первое из них задает источник данных для объекта Recordset. Данные в этом источнике могут представлять совокупность именованных разделов, называемых элементами источника данных - DataMember. Второе свойство и определяет конкретный элемент источника данных. В одной из последующих глав, посвященных компонентам OWC (Office Web Components), я расскажу подробнее и приведу примеры работы с подобными источниками данных и их элементами.
Property EditMode As EditModeEnum. Свойство указывает статус редактирования текущей записи. Его возможные значения: adEditAdd, adEditDelete, adEditInProgress, adEditNone указывают, была ли запись добавлена, удалена, операция редактирования записи не завершена или не применялась. Анализируя значение этого свойства, можно принять правильное решение и вызвать, например, метод Update или CancelUpdate, чтобы принять или отменить результаты редактирования.
Property Fields As Fields. Одно из немногих свойств, возвращающих объект - коллекцию полей, элементами которой являются объекты класса Field. Каждый такой объект задает одно поле в записи, а вся коллекция - совокупность всех полей. Позже я подробнее расскажу об этих объектах, о том, что можно делать с самой коллекцией, можно ли добавлять и удалять поля, о свойствах и методах, доступных при работе с отдельным полем.
Property Filter As Variant. Это свойство позволяет наложить фильтр на набор записей и создать новый набор, состоящий из записей, удовлетворяющих условию фильтра. При задании корректного фильтра текущей записью становится первая запись нового набора, перемещение по набору идет только по отфильтрованным записям, остальные записи как бы скрываются и становятся недоступными до тех пор, пока фильтр не будет отменен. Задать фильтр можно тремя различными способами:
Строкой критерия - представляет набор из элементарных условий, связанных логическими операциями AND и OR. Каждое элементарное условие имеет вид: <Имя поля> Operator <Значение>, где Operator является одной из операций сравнения или оператором Like, задающим сравнение с образцом. Образец может включать обычные для образцов метасимволы "?" и "*". Заметьте, на операциях AND и OR не задан приоритет, по этой причине для сложных критериев необходимо правильно задать скобочную запись критерия. Критерий должен представлять дизъюнкцию конъюнктов, это означает, что выражения в скобках должны соединяться знаком AND, а сами скобки знаком операции OR.
Массивом закладок - каждая закладка, как я говорил выше, определяет одну запись, а их массив задает все записи, которые должны быть отобраны в результате применения фильтра.
Стандартным фильтром - константой из перечисления FilterGroupEnum. Возможные значения в этом перечислении: adFilterAffectedRecords, adFilterConflictingRecords, adFilterFetchedRecords, adFilterPendingRecords, adFilterNone.
Приведу пример создания фильтра: Public Sub CreateFilter() 'Работа с фильтром
'Создать соединение CreateConnection 'Создать и выполнить команду 'задание свойств объекта Command Cmd1.ActiveConnection = Con1 Cmd1.CommandText = "SElect * From [Заказы]" Cmd1.CommandType = adCmdText Cmd1.Prepared = True ' вызов команды на исполнение Set Rst1 = Cmd1.Execute
'Задать фильтр Rst1.Filter = " [Сотрудник] = 'Петрова О.' AND " _ & "[Заказчик] = 'Книжная лавка'" With Rst1 .MoveFirst Do While Not .EOF 'Обработка текущей записи Debug.Print "Стоимость заказа = ", Rst1!Стоимость .MoveNext Loop End With End Sub
По-видимому, в дополнительных комментариях текст процедуры не нуждается. Не буду приводить и отладочных результатов, замечу, что все работало правильно, и на печать была выдана стоимость всех заказов, удовлетворяющих условию фильтра. Продолжим рассмотрение свойств:
Property Index As String. Устанавливает или возвращает строку, задающую имя индекса. Поля в таблицах базы данных могут быть индексированы. Система индексов позволяет быстрее находить нужные записи. Свойство Index используется в сочетании с методом поиска Seek для быстрого поиска записей в наборе по значениям индексируемого поля. Замечу, что не все Провайдеры позволяют работать с индексами, в частности Провайдер, обеспечивающий работу с базой данных Access, не поддерживает индексы.
Property MarshalOptions As MarshalOptionsEnum. Свойство позволяет управлять транспортировкой измененных записей набора на сервер. Применяется оно только на клиентской стороне и позволяет указать, нужно ли передавать серверу все записи или только те записи, которые подверглись изменениям. Свойство имеет всего два возможных значения: adMarshalAll, adMarshalModifiedOnly.
Property MaxRecords As Long. Позволяет ограничить число записей, одновременно передаваемых Провайдером от источника данных объекту Recordset. По умолчанию свойство имеет значение 0, указывающее, что ограничений на число передаваемых записей не устанавливается.
Property Properties As Properties. Второе из свойств, возвращающее объект - коллекцию Properties объекта Recordset. В этой большой коллекции, насчитывающей около сотни элементов - объектов класса Property собраны характеристики объекта Recordset, меняя которые можно управлять объектом. Многие из них задаются по умолчанию, обращение к ним позволяет выяснить текущие свойства объекта.
Property Sort As String. Если свойство Filter позволяет провести фильтрацию набора записей, то свойство Sort позволяет провести сортировку по одному или нескольким полям в порядке возрастания или убывания значений. Часто бывает удобнее сортировку и фильтрацию задавать не в операторе SQL при формировании объекта Recordset, а выполнять эти операции по мере необходимости, работая с уже созданным объектом и используя возможности, предоставляемые его свойствами. Вот пример подобной сортировки:
Public Sub CreateSortedSet() 'Сортировать полученный набор данных 'Создать соединение CreateConnection 'Создать и выполнить команду 'задание свойств объекта Command Cmd1.ActiveConnection = Con1 Cmd1.CommandText = "Select * From [Заказчики]" Cmd1.CommandType = adCmdText Cmd1.Prepared = True ' вызов команды на исполнение Set Rst1 = Cmd1.Execute 'Задание критерия сортировки Rst1.Sort = "Город ASC" 'Работа с отсортированным набором With Rst1 .MoveFirst Do While Not .EOF 'Обработка текущей записи Debug.Print "Организация = ", Rst1!Название, Rst1!Город .MoveNext Loop
End With End Sub
Для простоты я ограничился сортировкой по одному полю, указав ASC - возрастающий порядок следования значений по этому полю.
Property Source As Variant. Свойство позволяет задать или выяснить, что является источником данных для объекта Recordset. При установке значением свойства может быть ссылка на объект Command или строка. Возвращается всегда только строка, если источником данных является объект Command, то строка содержит описание источника - соответствующий SQL-оператор. Источником может быть также имя таблицы или имя хранимой процедуры.
Property State As Long, Property Status As Long. Я уже рассказывал о свойстве State, составное значение которого позволяет определить, открыт или закрыт объект и состояние выполняемого метода. Свойство Status в отличие от свойства State характерно только для объекта Recordset и определяет статус текущей записи по отношению к пакетным обновлениям или другим групповым операциям. Также как и для свойства State, его значение представляет сумму значений констант, в совокупности, задающих статус записи. Константы принадлежат перечислению RecordStatusEnum, задающему 18 различных значений, приведу лишь несколько из них: adRecNew, adRecInvalid, adRecUnmodified. Заметьте, как обычно в таких случаях, значения констант задаются степенями числа 2, так что их сумма всегда представляет уникальное значение и однозначно определяет все слагаемые. Программисты говорят в таких ситуациях, что значение свойства задает маску.
Property StayInSync As Boolean. Система ADO позволяет создавать иерархические наборы записей, в которых запись объекта Recordset может ссылаться на своих потомков. Такие иерархические наборы поддерживаются специальной службой - Microsoft Data Shaping Service for OLE DB. Булево свойство StayInSync задается только для иерархических наборов и позволяет управлять навигацией по иерархии записей в таком наборе. Значение True указывает, что при изменении позиции родительской записи раздел (Chapter), содержащий потомков, будет указывать на новое положение родителя. При значении False ссылка не меняется, так что потомки получают нового родителя.
Свойства объекта Recordset
Объект Recordset имеет несколько десятков свойств. Попробуем разобраться в них:
Property AbsolutePage As PositionEnum, Property AbsolutePosition As PositionEnum, Property PageCount As Long, Property PageSize As Long, Property RecordCount As Long - группа свойств, определяющих нумерацию записей в наборе. Поскольку объект Recordset может определять достаточно большой набор записей, то, иногда целесообразно придать ему определенную структуру и выделить в нем страницы, на каждой из которых находится фиксированное число записей. Исключением является последняя страница, на которой записей, естественно, может быть меньше. Свойство PageSize определяет число записей на странице, по умолчанию значение этого свойства равно 10. Свойство PageCount задает число страниц, AbsolutePage - номер страницы, на которой расположена текущая запись набора. Значением этого свойства является целое в интервале от 1 до PageCount или значение из перечисления PositionEnum. Свойство RecordCount задает число записей в наборе, а свойство AbsolutePosition - порядковый номер записи, значением которого есть целое в и нтервале от 1 до RecordCount или одно из значений перечисления PositionEnum. Это перечисление содержит всего три значения: adPosBOF, adPosEOF, adPosUnknown, первые два из которых определяют позицию в начале и конце набора (перед первой и после последней записи набора), а третье значение задает неопределенную позицию. Заметьте, что пользоваться номерами записей для их идентификации следует с большой осторожностью, поскольку нумерация изменяется, когда в набор добавляются или из него удаляются записи. Для идентификации записей используются закладки, о которых будет сказано чуть ниже. Не все Провайдеры полностью поддерживают эти свойства.
Property ActiveCommand As Object, Property ActiveConnection As Variant. Свойства отражают связи между объектами ADO. Значением этих свойств является ссылка на объект Command, породивший набор записей и ссылка на соединение, при котором команда выполнялась. Если соединение закрыто, то выдается строка, описывающая соединение. Если набор записей создавался без явного выполнения команды, то возвращается ссылка на пустой объект Null.
Property BOF As Boolean, Property EOF As Boolean. Их имена пришли из соответствующих названий при работе с файлами - Begin Of File (BOF) и End Of File (EOF). Два булевых свойства, позволяющих определить достигнут ли конец набора записей при его последовательном просмотре в том или ином направлении. Вот пример двух классических схем прохода набора записей:
Public Sub AllRecords() ' Две схемы прохода по набору записей With Rst1 'Схема1: От начала к концу набора .MoveFirst Do While Not .EOF 'Обработка текущей записи Debug.Print Rst1!Название .MoveNext Loop 'Схема2: От конца к началу набора .MoveLast Do 'Обработка текущей записи Debug.Print Rst1!Название .MovePrevious Loop Until .BOF End With End Sub
Я запустил на выполнение эту процедуру сразу по окончании работы процедуры CreateCommands из предыдущего примера и получил корректные результаты.
Property Bookmark As Variant. Часто в наборе записей необходимо иметь некоторое количество выделенных записей и периодически к ним обращаться. Как я уже говорил, номера записей для этой цели не годятся, поскольку они изменяются вместе с изменением самого набора. Для идентификации записей используются закладки, однозначно идентифицирующие запись при всех изменениях набора.
Когда открывается набор записей, то все записи имеют уникальные закладки. Сохранение закладок - дело рук программиста. Вы можете сохранить закладку в собственной переменной типа Variant, используя значение свойства Bookmark. В тот момент, когда значение этой переменной будет присвоено свойству Bookmark набора записей, автоматически текущей станет запись с указанной закладкой. Вот пример, иллюстрирующий работу с закладками: Public Sub CreateBookmarks() 'Работа с закладкой Dim MyBookmark As Variant With Rst1 .MoveFirst Do While Not .EOF 'Обработка текущей записи If Rst1!Название = "Книжная лавка" Then 'Создаю закладку MyBookmark = .Bookmark End If .MoveNext Loop 'переход к записи с закладкой .Bookmark = MyBookmark Debug.Print Rst1!Название End With End Sub
Заметьте, по окончании цикла, текущая запись не определена, поскольку истинно свойство EOF, и это означает, что указатель сместился за последнюю запись. Но как только свойству Bookmark присвоено значение сохраненной закладки, текущей становится нужная нам запись.
Property CacheSize As Long. Я уже говорил, что основная работа с записями базы данных ведется во временной памяти, называемой буфером или кэшем. Несмотря на название, это свойство явно не задает размер памяти буфера. Оно должно быть целым значением в интервале от 1 до Maximum Open Rows. Правая граница интервала задается одноименным свойством из коллекции Properties объекта Recordset. Заметьте, значение 0 приводит к ошибке, по умолчанию значение свойства равно 1. Свойство позволяет указать число записей, помещаемых Провайдером в кэш при одном обращении. Заметьте, идет постоянный своппинг (обмен данными) в процессе работы, поскольку, когда достигнута последняя запись в буфере и нужна следующая запись, будет идти подкачка в буфер очередной порции. Значение свойства настраивается в течение жизни объекта Recordset.
Универсальный доступ к данным и ADO
Microsoft предлагает своим пользователям и, прежде всего, разработчикам корпоративных приложений единую стратегию доступа к данным - Universal Data Access (UDA), не зависящую, в высокой степени, от типа используемых хранилищ данных, применяемых инструментальных средств и языков программирования. В основе этой стратегии лежат компоненты доступа к данным - Microsoft Data Access Components (MDAC). Три базисных интерфейса и, соответственно, три группы объектов определяют UDA и MDAC:
Низко уровневый, высоко эффективный по производительности COM-интерфейс баз данных - интерфейс OLE DB (OLE Data Base), позволяющий получать доступ как к реляционным (табличным), так и не реляционным (иерархическим и потоковым) базам данных.
Низко уровневый, высоко производительный интерфейс ODBC (Open Data Base Connectivity), специально спроектированный для связи с различными реляционными базами данных и структурированным языком запросов SQL (Structured Query Language).
Интерфейс высокого уровня ADO, являющийся надстройкой над интерфейсом OLE DB, позволяющий получать легкий доступ к данным при работе не только на VC, но и на VB/VBA и VB Script.
Начиная с Windows 2000, MDAC является частью операционной системы. Microsoft создала специальный Web-узел - Microsoft Universal Data Access Web site, на котором можно найти последние версии компонент и свободно произвести обновление операционной системы от Windows 95 до Windows Me. Поскольку для VBA - разработчика, как я уже говорил, наибольший интерес представляет интерфейс верхнего уровня - ADO, то я ограничусь лишь штрихами к портрету низкоуровневых интерфейсов.
Мир объектов Excel 2000
ADO и DAO
До появления объектов ADO при работе с базой данных Access использовались объекты DAO (Data Access Objects), являющиеся предшественниками ADO. Они и сейчас, естественно, могут использоваться в уже разработанных и действующих приложениях. В новых разработках следует отдать предпочтение объектам ADO. Объектные модели ADO и DAO хотя и имеют много общего, но имеют и существенные различия. Так что вряд ли целесообразно переходить к новым объектам в уже работающих приложениях, если это не вызвано особыми причинами. В этом разделе я приведу сравнение объектных моделей и другие сведения, облегчающие переход от DAO к ADO в случае, если такая необходимость возникла.
Анализ ошибок в обработчике событий
Я хочу теперь рассмотреть ситуации, когда при выполнении операции по тем или иным причинам возникают ошибки. Мы рассмотрим, как выглядят обработчики ошибок в таких ситуациях, как можно проанализировать содержимое объектов Error, созданных Провайдером и переданных обработчику ошибок, как можно исправить ситуацию и продолжить выполнение. Первым делом я смоделировал ситуацию, приводящую к ошибке в момент создания соединения, для чего сменил свойство "Provider", не позаботившись о корректном задании характеристик этого Провайдера. Вот как выглядит модернизованная процедура, организующая соединение: Public Sub ConnectionWithEvents() 'Создание соединения с тестовой базой данных Access 'Объект с событиями Dim imEvCon As New MyEventsADO ' Связывание объекта с событиями Set imEvCon.EvCon = Con1 Dim strConnStr As String If Con1.State = adStateOpen Then Con1.Close 'закрыть соединение 'Вариант 1 'Конфигурирование соединения Con1 'Con1.Provider = "Microsoft.jet.oledb.4.0" Con1.Provider = "MSDASQL"
Con1.ConnectionString = "Data Source=c:\!O2000\DsCd\Ch14\dbPP2000.mdb" Con1.CursorLocation = adUseClient 'Открытие соединения On Error GoTo Check Con1.Open Exit Sub Check: 'Повтор операции соединения Resume End Sub
В этой процедуре следует обратить внимание на следующие моменты:
"Правильный" Провайдер, позволяющий работать с базой данных Access закомментирован. Указан Провайдер "MSDASQL", - его вызов приведет к ошибке при установлении соединения. Для удобства восприятия текст этой строки и других строк, требующих внимания, подсвечен.
Чтобы иметь возможность написать обработчики событий, возникающих при соединении, введен соответствующий объект imEvCon класса MyEventsADO, о создании которого я говорил выше.
Предусмотрена возможность обработки возникающих ошибок. О том, как исправить возникающую ошибку, скажу чуть ниже.
В класс MyEventsADO я добавил следующий обработчик события ConnectComplete: Private Sub EvCon_ConnectComplete(ByVal pError As ADODB.Error, _ adStatus As ADODB.EventStatusEnum, _ ByVal pConnection As ADODB.Connection) 'Печать параметров обработчика события MsgBox "Обработчик события ConnectComplete! " & vbCrLf & _ "Строка соединения = " & _ pConnection.ConnectionString & vbCrLf & _ "Статус выполняемой операции = " & adStatus 'Печать параметров объекта Error If Not (pError Is Nothing) Then With pError MsgBox "Описание ошибки:" & .Description & vbCrLf & _ "Код Провайдера:" & .NativeError & vbCrLf & _ "Константа, идентифицирующая ошибку:" & .Number & vbCrLf & _ "Имя объекта:" & .Source & vbCrLf & _ "ANSI SQL код:" & .SqlState End With End If If adStatus = adStatusErrorsOccurred Then 'Исправление ошибки pConnection.Provider = "Microsoft.jet.oledb.4.0" End If 'Анализ коллекции Errors Dim myErr As Error Dim MyCon As Connection Set MyCon = pConnection Debug.Print MyCon.Errors.Count For Each myErr In MyCon.Errors Debug.Print myErr.Description Next myErr MyCon.Errors.Clear Debug.Print MyCon.Errors.Count MyCon.Errors.Refresh Debug.Print MyCon.Errors.Count For Each myErr In MyCon.Errors Debug.Print myErr.Description Next myErr End Sub
Прокомментирую работу этой процедуры:
Вначале создается диалоговое окно функции MsgBox, в котором выводится информация о входных параметрах обработчика соединения. На рис. 6.3 можно увидеть, как выглядит это окно и следующие два окна сообщений, связанных с открытием соединения.
Рис. 6.3. Окна сообщений, появляющиеся при установлении соединения
Обратите внимание, статус показывает, что в момент соединения возникла ошибка, поэтому строка соединения короткая и не содержит информации, добавляемой Провайдером.
Следующая часть обработчика ConnectComplete работает тогда, когда обработчику передается объект Error. В функции MsgBox, окно которой также показано на рис. 6.3, формируется информация о свойствах объекта Error, в частности, описание ошибки показывает, что ODBC-Провайдер не сумел найти источник данных.
В обработчике события "исправляется" ошибка и устанавливается "правильный" Провайдер. Конечно, в реальной ситуации для этого мог понадобиться диалог с пользователем, но я решил упростить демонстрационный пример. В процедуре есть еще одна часть, связанная с выдачей отладочной информации, но о ней чуть позже, а пока скажу, что произойдет по окончании работы обработчика событий. Понятно, что когда управление вернется в процедуру ConnectionWithEvents в оператор Con1.Open, вызвавший событие, то из-за возникновения ошибки в охраняемом блоке управление будет перехвачено и передано на метку Check. Оператор Resume заставит повторить открытие соединения, но теперь уже свойства соединения заданы корректно. Снова работает процедура ConnectComplete, но в ситуации, когда ошибка не возникает. В этот раз будет работать только первая функция MsgBox, которая уведомит о нормальном завершении и выдаст подробную информацию в строке соединения.
В заключительной части процедуры ConnectComplete выводится на печать отладочная информация о коллекции Errors объекта Connection. Я специально поместил ее в эту процедуру, чтобы обратить Ваше внимание на тот факт, что при обработке события Complete коллекция Errors объекта Connection еще не сформирована, она создается лишь после завершения работы этой процедуры. Поэтому, как ни странно, но при первом вызове, когда ошибка есть, коллекция Errors пуста, а во втором вызове, когда ошибки нет, коллекция существует, сохраняя историю первого вызова. Заметьте, по этой причине было бы разумнее с содержательной точки зрения поместить эту группу операторов в блок с меткой Check. Иллюстрацией к этому тексту являются результаты отладочной печати:
0 0 0 1 [Microsoft][ODBC Driver Manager] Data source name not found and no default driver specified 0 0
Еще одна ошибка, которую я решил смоделировать, возникает при работе с объектом Recordset, - некорректно формируется значение поля при изменении записи. К уже имеющемуся обработчику события WillChangeField я добавил обработчик события ChangeFieldComplete. Поскольку общая схема экспериментов остается такой же, как и для объекта Connection, то я приведу тексты процедур без дополнительных комментариев. В уже описанной процедуре CreateEvents я заменил оператор
!Цена = !Цена * 2
на оператор
!Цена = "По Договоренности"
Эта замена приводит к ошибке несоответствия типов. Вот текст обработчика события ChangeFieldComplete:
Private Sub EvRst_FieldChangeComplete(ByVal cFields As Long, _ ByVal Fields As Variant, ByVal pError As ADODB.Error, _ adStatus As ADODB.EventStatusEnum, _ ByVal pRecordset As ADODB.Recordset) 'Печать параметров обработчика события MsgBox "Номер поля = " & cFields & vbCrLf & _ "Значение поля = " & Fields(cField) & vbCrLf & _ "Позиция записи в наборе = " & _ pRecordset.AbsolutePosition & vbCrLf & _ "Статус выполняемой операции = " & adStatus 'Печать параметров объекта Error If Not (pError Is Nothing) Then With pError MsgBox "Описание ошибки:" & .Description & vbCrLf & _ "Код Провайдера:" & .NativeError & vbCrLf & _ "Константа, идентифицирующая ошибку:" & .Number & vbCrLf & _ "Имя объекта:" & .Source & vbCrLf & _ "ANSI SQL код:" & .SqlState End With End If 'Анализ коллекции Errors Dim myErr As Error Dim MyCon As Connection Set MyCon = pRecordset.ActiveConnection Debug.Print MyCon.Errors.Count For Each myErr In MyCon.Errors Debug.Print myErr.Description Next myErr
End Sub
Вот как выглядит серия появляющихся окон функции MsgBox, вызываемой в процедурах: CreateEvents, WillChangeField, ChangeFieldComplete, при попытке изменить поле "Цена":
Рис. 6.4. Серия окон, появляющихся при попытке изменить значение поля.
Возможно, полезно взглянуть и на информацию, появляющуюся в окне отладки:
0 1 Operation was canceled. 1 Operation was canceled. 1 Multiple-step operation generated errors. Check each status value.
Добавление индексов
В следующем примере в таблицу "Книги" я добавляю два индекса, один из них связан с полем "Автор", другой - "Название_книги". Приведу процедуру, решающую задачу: Public Sub AddIndex() 'Процедура добавляет индексы в таблицу "Книги" Dim myT As ADOX.Table 'таблица Dim myC1 As Column, myC2 As Column 'поля таблицы Dim myInd1 As New Index, myInd2 As New Index 'индексы таблицы 'Установить соединение с базой NewDB CreateConnection Cat1.ActiveConnection = Con1 'Связывание объекта Table с таблицей "Книги" Set myT = Cat1.Tables("Книги") Set myC1 = myT.Columns("Автор") Set myC2 = myT.Columns("Название_книги") 'Формирование индексов myInd1.Columns.Append (myC1) myInd1.Name = "Author" myInd1.PrimaryKey = False myInd1.Unique = False myInd1.Clustered = False
myInd2.Columns.Append (myC2) myInd2.Name = "Book" myInd2.PrimaryKey = False myInd2.Unique = False myInd2.Clustered = False
'Добавление индексов Call myT.Indexes.Append(myInd1) Call myT.Indexes.Append(myInd2) 'Отладочная печать Debug.Print "Число индексов =", myT.Indexes.Count For Each myInd1 In myT.Indexes Debug.Print "Имя индекса -", myInd1.Name Next myInd1
End Sub
Результаты отладочной печати для нее следующие: Число индексов = 3 Имя индекса - Код_книги Имя индекса - Author Имя индекса - Book
Заметьте, индексов три, поскольку первый из них был создан автоматически в момент добавления ключа в таблицу. Думаю, что в особых комментариях данная процедура не нуждается, поскольку работа с индексами ведется по той же схеме, что и при работе с ключами таблицы. Чтобы убедиться, что все работает должным образом, взгляните, как выглядит таблица "Книги", открытая в режиме конструктора:
Рис. 6.9. Программно созданная таблица "Книги"
Добавление полей и ключа
В следующей процедуре в уже созданную таблицу добавляется новое поле и новый составной ключ. Приведу текст процедуры: Public Sub InsertItems() 'Эта процедура добавляет элементы в уже созданную таблицу Dim myT As Table Dim myC As New Column Dim myK As New Key Dim myInd As Index 'Установить соединение с базой NewDB CreateConnection Cat1.ActiveConnection = Con1 'Связывание объекта Table с таблицей "Книги" Set myT = Cat1.Tables("Книги") 'Добавление поля myC.ParentCatalog = Cat1 myC.Name = "Художник" myC.Attributes = adColNullable Call myT.Columns.Append(myC)
'Добавление составного ключа myK.Columns.Append (myT.Columns("Автор")) myK.Columns.Append (myT.Columns("Название_книги")) myK.RelatedTable = "myT" myK.Type = adKeyUnique myK.Name = "AandB" Call myT.Keys.Append(myK) End Sub
Все эти добавления в таблицу возможны и проходят без препятствий. Заметьте, добавление составного ключа приведет к добавлению элемента в коллекцию индексов. Взгляните, как выглядит измененная таблица "Книги". Здесь же на рисунке можно увидеть и открытое окно с индексами таблицы, в котором отображается введенный составной ключ.
увеличить изображение Рис. 6.10. Изменения полей и индексов таблицы "Книги"
Другие объекты модели ADO
Свойства, методы и события основных объектов ADO - Connection, Command, Recordset, Error уже рассмотрены. В примерах появлялись и другие объекты - Field, Parameter, Property и их коллекции. Рассмотрим свойства и методы этих и других объектов ADO, но уже с меньшей степенью детализации.
Другие Провайдеры
Краткие данные о других Провайдерах приведу в таблице.
Таблица 6.1. Характеристики Провайдеров
Название ПровайдераСтрока соединенияНазначение
>Microsoft OLE DB Remoting Provider
"Provider=MS Remote"
Позволяет удаленному пользователю подключиться к источнику данных так, как если бы он был локальным пользователем.
Microsoft OLE DB Simple Provider (OSP)
"Provider=MSDAOSP;Data Source=serverName"
Позволяет ADO получить доступ к любым данным, для которых Провайдер написан с использованием специального инструментария - OLE DB Simple Provider Toolkit. Такие источники данных требуют лишь базисных средств поддержки OLE DB. Типичным примером данных являются - XML документы.
Microsoft OLE DB Persistence Provider
"Provider=MSPersist"
Этот Провайдер позволяет сохранить в файле данные и всю необходимую информацию об объекте Recordset, в том числе и иерархическом. Затем, при необходимости, объект Recordset может быть восстановлен.
Microsoft Data Shaping Service for OLE DB
"Provider=MSDataShape"
Поддерживает конструирование иерархических объектов Recordset.
Microsoft OLE DB Provider for Microsoft Indexing Service
"Provider=MSIDXS;Data Source=myCatalog;Locale Identifier=nnnn;"
Типично указывается только первый параметр. Второй параметр задает имя каталога специальной поисковой службы - Indexing Service. Если параметр опущен, то по умолчанию используется системный каталог. Параметр Locale Identifier указывает предпочтения при форматировании данных, заданные пользовательским языком.
Поддерживает программный доступ со статусом "только для чтения" к файловой системе и Web-страницам, индексируемых службой Microsoft Indexing Service.
Microsoft Active Directory Service Interfaces (ADSI) Provider
to connect to heterogeneous directory services through ADSI. This gives ADO applications read-only access to the Microsoft Windows NT® 4.0 and Microsoft Windows 2000 directory services, in addition to any LDAP-compliant directory service and Novell Directory Services. ADSI itself is based on a provider model, so if there is a new provider giving access to another directory, the ADO application will be able to access it seamlessly.
Позволяет связаться с гетерогенными службами каталогов - Windows NT 4.0, Windows 2000, Novell Directory Services, LDAP Directory Services.
Microsoft Cursor Service for OLE DB
Строки соединения нет, поскольку это специальная служба, вызываемая автоматически в зависимости от расположения курсора.
Эта специальная служба обеспечивает функции поддержки курсора для различных Провайдеров данных, что обеспечивает единую функциональность в работе ADO. Вызывается служба автоматически, если соответствующим образом задать положение курсора объектов Connection или Recordset: connection.CursorLocation=adUseClient recordset.CursorLocation=adUseClient
Коллекции Tables, Procedures, Views, Groups, Users
С объектной точки зрения все эти коллекции устроены одинаково. Они хранят соответствующие элементы классов: Table, Procedure, View, Group, User. У них есть три метода и два свойства. Для добавления нового элемента в коллекцию применяется метод Append(item), удаления - метод Delete(item), для обновления состояния коллекции, учитывающего текущее состояние базы данных - метод Refresh(). Свойство Item позволяет добраться до нужного элемента коллекции, а свойство Count возвращает число элементов в коллекции. Некоторая особенность связана с коллекциями Groups и Users. Коллекция Groups объекта Catalog задает все группы базы данных. С другой стороны, как показано на схеме, отражающей связи между объектами (Рис. 7), коллекция Groups связана и с отдельным объектом User. Коллекция групп, связанная с отдельным пользователем, возвращает все группы, в которые входит этот пользователь. Заметьте, прежде чем добавить новую группу с именем name в коллекцию Groups объекта User, элемент с таким именем уже должен находиться в коллекции Groups объекта Catalog. Совершенно симметрично дело обстоит и с коллекцией Users, которая в объекте Catalog описывает всех пользователей, а в объекте Group - пользователей данной группы. Аналогично, прежде чем добавить пользователя в отдельную группу, он должен быть добавлен в коллекцию Users объекта Catalog .
Коллекция Errors и объекты Error
Напомню, что элементы коллекции Errors появляются при выполнении очередной команды, когда Провайдер сталкивается с определенными трудностями и не может нормальным образом завершить выполнение команды. Коллекция Errors всегда связана только с одной командой, - при первой ошибке в новой команде происходит чистка старого содержимого коллекции. Свойство Errors объекта Connection возвращает эту коллекцию. Добавлять программно элементы в коллекцию невозможно, это делается автоматически, когда Провайдер обнаруживает новую ошибку.
Коллекция Fields и объект Field
Свойство Fields объектов Recordset и Record возвращает одноименную коллекцию, элементами которой являются объекты Field, представляющие содержательно поля записи или столбцы данных, пользуясь табличной терминологией. Об этих объектах уже было сказано немало, так что я постараюсь долго на них не останавливаться. У коллекции Fields всего два традиционных для всех коллекций свойства - Count и Item, а вот методов гораздо больше. Наряду с методами, о которых я уже говорил при рассмотрении других объектов ADO, такими как Delete, Refresh, Update, CancelUpdate, имеется два специфических метода:
Sub Append(Name As String, Type As DataTypeEnum, [DefinedSize As Long], [Attrib As FieldAttributeEnum = adFldUnspecified], [FieldValue]). Метод позволяет создать новое поле и присоединить его к коллекции. При определении поля задается необходимая информация - имя поля, его тип, определяемый размер данных, атрибуты и, возможно, значение, если речь идет об объекте Record. При работе с полями объекта Recordset они должны быть созданы при закрытом объекте Recordset, после этого объект можно открыть и присвоить значения созданным полям. Присоединять поля к объекту Recordset можно только в том случае, если он закрыт и для него не установлено свойство ActiveConnection. Поскольку в реальных ситуациях эти объекты появляются при выполнении метода Open или Execute, когда соединение с источником данных установлено, то присоединять новые поля к объекту Recordset таким способом не удается. Для изменения структуры базы данных используются обычно объекты ADOX, которые будут рассмотрены ниже. Так что, чаще всего, метод Append применяется при работе с полями объекта Record. Добавлять поле к этому объекту можно и тогда, когда он открыт, при добавлении можно указать и значение поля. После добавления поля следует вызвать метод Update, чтобы сохранить сделанные изменения.
Sub Resync([ResyncValues As ResyncEnum = adResyncAllValues]). Позволяет обновить значения всех полей у записей, хранящихся в буфере.
Коллекция Parameters и объект Parameter
Объект Command может определять запрос или хранимую процедуру с параметрами. А посему этот объект имеет свойство Parameters, возвращающее одноименную коллекцию, элементами которой являются объекты Parameter. Содержательно, каждый из этих объектов определяет параметр - входной или выходной, который передается или возвращается при вызове хранимой процедуры. Я уже приводил пример вызова запроса с параметром, где появлялись эти объекты. Коллекция Parameters устроена достаточно просто. У нее всего два типичных свойства - Item и Count. У коллекции три метода, которые также появлялись в нашем рассмотрении. Вот эти методы:
Sub Append(Object As Object). Метод позволяет присоединить объект Parameter, заданный аргументом, к коллекции. Обычно присоединяемый объект создается методом CreateParameter объекта Command. В процедуре CreateCommands, приведенной в предыдущей главе, продемонстрировано применение методов CreateParameter и Append.
Sub Delete(Index). Метод позволяет удалить из коллекции параметр, по имени или порядковому номеру, указываемому в аргументе Index.
Sub Refresh(). Позволяет обновить содержимое коллекции, получая информацию о параметрах из хранимой процедуры.
Коллекция Properties и объект Property
Многие из объектов ADO имеют свойство Properties, возвращающее одноименную коллекцию элементов Property. Поскольку каждый объект обладает набором свойств, то возникает естественный вопрос, зачем необходимо еще и свойство Properties, которое тоже задает свойства. Дело в том, что свойства разделяются на встроенные и динамические. Динамические свойства зависят от специфики Провайдера, - они то и составляют коллекцию Properties. Каждый объект этой коллекции описывает то или иное динамическое свойство объекта ADO, зависящее от специфики Провайдера и доступное, естественно, только тогда, когда объект открыт. Добавлять или удалять элементы этой коллекции невозможно, - это прерогатива Провайдера. С объектной точки зрения коллекция Properties устроена очень просто - у нее всего два типичных свойства: Item, Count и один метод Refresh. Также просто устроен и объект Property - у него всего четыре свойства: Name, Type, Value, Attributes. Они задают имя, тип, значение и некоторые дополнительные характеристики. Примеры работы с этими объектами уже приводились. На этом я завершаю утомительное описание объектов ADO. Но нам предстоит еще хотя бы вкратце познакомиться с объектами ADOX.
Методы объекта Catalog
У этого объекта три метода:
Function Create(ConnectString As String). Позволяет создать новую базу данных. Параметр ConnectString задает строку соединения. Если создание базы данных прошло успешно, то в качестве результата возвращается объект Connection из библиотеки ADODB, задающий соединение с базой. Этот же объект становится значением свойства ActiveConnection объекта Catalog. Естественно, при выполнении метода возникнет ошибка, если Провайдер не поддерживает создание нового источника данных.
Function GetObjectOwner(ObjectName As String, ObjectType As ObjectTypeEnum, [ObjectTypeId]) As String.
Sub SetObjectOwner(ObjectName As String, ObjectType As ObjectTypeEnum, UserName As String, [ObjectTypeId]).
Эти два метода позволяют определить и, соответственно, установить собственника объекта. Параметр UserName метода Set задает имя собственника, которое возвращает метод (функция) Get. Параметры ObjectName и ObjectType задают имя и тип объекта, для которого устанавливается собственник. Наличие этих параметров связано с тем, что методы применимы не только к объекту Catalog. Тип объекта задается константами из перечисления ObjectTypeEnum. Вот их значения: adPermObjColumn, adPermObjDatabase, adPermObjProcedure, adPermObjTable, adPermObjView. Еще одно возможное значение - adPermObjProviderSpecific указывается тогда, когда объект не принадлежит ни к одному из указанных типов и зависит от Провайдера, в этой ситуации тип задается последним параметром ObjectTypeId. Чтобы оживить изложение, давайте перейдем к рассмотрению примеров. Для экспериментов с объектами ADOX я создал новый модуль. Глобальные переменные этого модуля будут определять уже знакомые объекты - Connection, Command, Recordset. Но к ним теперь добавится и новый объект, задающий Catalog. Option Explicit 'Модуль TestingADOX 'Глобальные переменные Public Con1 As New ADODB.Connection Public Cmd1 As New ADODB.Command Public Rst1 As New ADODB.Recordset Public Strm1 As New ADODB.Stream Public Cat1 As New ADOX.Catalog
Вот как выглядит первая процедура этого модуля, создающая новую базу данных: Public Sub CreateNewDB() 'Создание новой базы данных и открытие соединения с ней Dim strConnStr As String strConnStr = "Provider=Microsoft.jet.oledb.4.0;" & _ "Data Source=c:\!O2000\DsCd\Ch16\NewDB.mdb" Set Con1 = Cat1.Create(strConnStr) Debug.Print Cat1.ActiveConnection Dim Con2 As Connection Set Con2 = Cat1.ActiveConnection Debug.Print Con2.ConnectionString End Sub
В этой процедуре я создал новую базу данных Access и установил с ней соединение. Заметьте, я ввел два объекта класса Connection, - один из них определяется при вызове метода Create объекта Cat1 класса Catalog, а второй определяется, используя значение свойства ActiveConnection. Отладочная печать в обоих случаях одинакова и дает следующий результат:
Sub AppendChunk(Data). Этот метод позволяет формировать "длинные" значения полей, присоединяя очередную порцию данных к текстовому или двоичному значению поля. При первом вызове метода значение, заданное аргументом Data переписывается в поле и становится его значением, при последующих вызовах значение аргумента Data добавляется к значению, хранимому в поле. Метод не применим к полям объекта Record. Аргумент Data задается переменной типа Variant и содержит добавляемые данные. Заметьте, для того чтобы можно было использовать этот метод бит adFldLong в свойстве Attributes должен быть установлен - иметь значение True.
Function GetChunk(Length As Long). Метод позволяет получить все значение или нужную порцию большого текста или двоичного кода, хранящегося в поле. Длина порции устанавливается параметром Length. Возвращаемый результат является переменной типа Variant.
Приведу теперь пример, в котором анализируются поля объекта Recordset. По изложенным выше причинам я не стал пытаться вводить новые поля и ограничился получением информации о свойствах уже существующих полей записи. Вот код соответствующей процедуры: Public Sub CreateField() 'Процедура печатает информацию о существующих полях записи Dim fld As Field Dim df As Object 'Создать соединение CreateConnection 'Создать команду 'задание свойств объекта Command Cmd1.ActiveConnection = Con1 Cmd1.CommandText = "Select * From [Книги]" Cmd1.CommandType = adCmdText 'Открытие обновляемого объекта Recordset With Rst1 .Open Source:=Cmd1, CursorType:=adOpenDynamic, _ LockType:=adLockOptimistic
.MoveFirst Do While Not .EOF 'Печать данных о полях записи набора If !Название = "Офисное программирование" Then For Each fld In .Fields With fld Debug.Print "Имя поля ", .Name Debug.Print "Фактический размер поля =", .ActualSize Debug.Print "Определяемый размер поля =", .DefinedSize Debug.Print "Точность", .Precision Debug.Print "Число цифр после запятой в числовых полях -", _ .NumericScale Debug.Print "Тип поля", .Type Set df = .DataFormat If df Is Nothing Then Debug.Print "объект - Формат данных -не определен " End If Debug.Print "Статус", .Status Debug.Print "Значение поля до его изменений ", .OriginalValue Debug.Print "Текущее значение в базе данных", .UnderlyingValue Debug.Print "Текущее значение поля в наборе", .Value Debug.Print "Атрибуты =", .Attributes Debug.Print "Число свойств =", .Properties.Count Debug.Print "Имя и значение первого свойства - ", _ .Properties(1).Name, .Properties(1).Value End With Next fld End If .MoveNext Loop 'попытка изменить характеристики полей 'закрываем объект Recordset .Close Set fld = .Fields("Год издания") With fld .DefinedSize = 8 .Type = adDecimal End With '.Fields.Update
End With End Sub
Приведу краткий комментарий:
Для выбранной записи из набора в окне отладки я печатаю значения всех характеристик для каждого поля записи.
Свойство Data Format возвращает ссылку на пустой объект.
Попытка изменить метаданные, путем изменения таких характеристик поля, как его тип или определяемый размер, не приводит к успеху. Хотя при закрытом объекте Recordset изменение значений этих характеристик возможно, но метод Fields.Update в данном контексте не работает, по этой причине он закомментирован.
Подведем теперь некоторые итоги. Вот что можно делать с объектом Field:
Свойство Name позволяет вернуть имя поля, но не позволяет изменить это имя.
Свойство Value позволяет не только получить значение поля, но и изменить его.
Свойства Type, Precision, NumericScale, DefinedSize позволяют получить, но не изменить метаданные, задающие соответствующие характеристики поля.
Свойство ActualSize позволяет определить фактический размер хранимого в поле значения.
Определить, какой тип функциональности поддерживается данным полем, используя свойства Attributes и Properties.
Работать с длинными полями, формируя или получая их значения порциями, используя методы AppendChunk и GetChunk.
Все свойства, задающие метаданные, доступны для изменения при закрытом объекте Recordset, что может быть полезно при динамическом конструировании форм.
Методы объекта Stream
Рассмотрим теперь методы объекта Stream:
Sub Cancel(). Как и для других объектов ADO позволяет прервать выполнение асинхронных операций над объектом Stream.
Sub Close(). Закрывает объект Stream.
Sub CopyTo(DestStream As Stream, [CharNumber As Long = -1]). Позволяет создать копию потока. Объект, представляющий точку назначения, должен быть открыт. Уточню семантику. Метод копирует, начиная с текущей позиции, символы (байты), число которых задано вторым параметром. Если раньше встретится конец потока, то копирование идет от текущей точки до конца потока. Если в потоке назначения есть символы, то они остаются, следуя после скопированной части. Чтобы произвести их отсечение, следует вызвать метод SetEOS для потока назначения.
Sub Flush(). Принуждает оставаться содержимому потока в ADO буфере того базового объекта, с которым ассоциирован объект Stream.
Sub LoadFromFile(FileName As String), Sub SaveToFile(FileName As String, [Options As SaveOptionsEnum = adSaveCreateNotExist]). Эти методы позволяют загрузить содержимое файла в поток и сохранить поток в файле.
Sub Open([Source], [Mode As ConnectModeEnum = adModeUnknown], [Options As StreamOpenOptionsEnum = adOpenStreamUnspecified], [UserName As String], [Password As String]). Один из основных методов, создающий поток. Первый параметр задает источник данных. Это может быть абсолютный URL-адрес, в этом случае параметр имеет следующий синтаксис "URL = scheme://server/folder". Адрес может задавать узел в структуре дерева каталогов файловой системы или системе электронной почты. Источник может быть ссылкой на открытый объект Record. Объект Stream ассоциируется с потоком по умолчанию этого объекта. В наших примерах использовался именно такой способ создания объекта Stream. Источник может и не указываться, тогда создается объект Stream, не ассоциированный с потоком другого объекта. Данные в него могут поступать, например, при загрузке из файла при вызове метода LoadFropmFile или при создании копии потока.
Function Read([NumBytes As Long = -1]), Function ReadText([NumChars As Long = -1]) As String, Sub Write(Buffer), Sub WriteText(Data As String, [Options As StreamWriteEnum = adWriteChar]). Эта группа методов позволяет читать и писать в поток заданное число байтов (символов). Методы Read и Write, работающие с байтами, используют переменную типа Variant, а методы ReadText и WriteText используют переменную типа String. Заметьте, речь идет о потоке, поэтому все операции по чтению и записи идут только в одном направлении - от начала к концу потока. Если требуется несколько проходов, то следует заново переоткрывать поток.
Sub SetEOS(). Метод устанавливает значение свойства EOS в True, а, главное, делает текущую позицию концом потока, отсекая все оставшиеся символы. Поскольку методы Write, WriteText, CopyTo не производят отсечения, то, чаще всего, они используются в комбинации с методом SetEOS.
Sub SkipLine(). Позволяет пропустить одну строку при чтении текстового файла. Используется в цикле, когда нужно пропустить несколько строк.
Модификация характеристик
К сожалению, в Access попытка программно изменить характеристики полей таблицы или ее ключей и индексов невозможна. Я специально написал процедуру, в которой делается такая попытка: Public Sub Modify() 'Изменение характеристик таблицы в Access невозможно! Dim myT As Table Dim myC As Column Dim myK As Key Dim myInd As Index 'Установить соединение с базой NewDB CreateConnection Cat1.ActiveConnection = Con1 'Связывание объекта Table с таблицей "Книги" Set myT = Cat1.Tables("Книги") Set myC = myT.Columns("Название_книги") 'myC.DefinedSize = 150 Set myC = myT.Columns("Число_страниц") 'myC.Type = adVarWChar
Set myK = myT.Keys("Код_книги") 'myK.Type = adKeyUnique Set myK = myT.Keys("AandB") 'myK.Type = adKeyPrimary End Sub
Как видите, все операторы, производящие изменения закомментированы, поскольку выполнение любого из них приводит к появлению ошибки. Хотя модификация характеристик полей, ключей и индексов таблицы базы данных Access невозможна, удаление самих элементов происходит без затруднений. Это позволяет при необходимости внесения изменений в характеристики элемента, вначале удалить его, а затем ввести заново с новыми характеристиками. Приведу процедуру, выполняющую удаление поля, ключа и индекса: Public Sub Removing() 'Удаление элементов таблицы Dim myT As Table 'Установить соединение с базой NewDB CreateConnection Cat1.ActiveConnection = Con1 'Связывание объекта Table с таблицей "Книги" Set myT = Cat1.Tables("Книги") myT.Columns.Delete ("Художник") myT.Indexes.Delete ("AandB") 'myT.Keys.Delete ("AandB") End Sub
Заметьте, когда я вводил ключ "AandB", то был создан индекс с этим же именем. Удалив этот индекс, я автоматически удалили и ключ. По этой причине последний оператор процедуры закомментирован. На этом я закончу рассмотрение объектов, связанных с таблицами базы данных и займусь другими объектами модели ADOX, которые позволяют работать с запросами, представлениями и пользователями.
Несколько примеров программной работы с таблицами
Пора привести пример программного создания таблицы и работы с объектами, связанными с таблицами базы данных. В предыдущих примерах уже была создана программно база данных с именем NewDB. Рассмотрим еще одну процедуру из модуля "TestingADOX", в которой в эту базу данных будет добавлена таблица "Книги", уже появлявшаяся в наших примерах. Но теперь создадим эту таблицу программно, используя возможности объектов ADOX. Вот текст процедуры, решающей эту задачу: Public Sub CreateTable() 'Создание таблицы "Книги" и присоединение ее к базе данных NewDB 'Создание объектов - таблицы, ее полей и ключа Dim myT As New ADOX.Table 'таблица Dim myC(1 To 6) As New ADOX.Column 'поля таблицы Dim myK As New ADOX.Key 'ключ таблицы 'Установить соединение с базой NewDB CreateConnection Cat1.ActiveConnection = Con1 'Создание объекта Table 'Создание полей таблицы myC(1).ParentCatalog = Cat1 myC(1).Name = "Код_книги" myC(1).Type = adInteger Call myT.Columns.Append(myC(1))
myC(2).ParentCatalog = Cat1 myC(2).Name = "Автор" Call myT.Columns.Append(myC(2))
myC(3).ParentCatalog = Cat1 myC(3).Name = "Название_книги" Call myT.Columns.Append(myC(3))
myC(4).ParentCatalog = Cat1 myC(4).Name = "Год_издания" Call myT.Columns.Append(myC(4))
myC(5).ParentCatalog = Cat1 myC(5).Name = "Число_страниц" myC(5).Type = adSmallInt Call myT.Columns.Append(myC(5))
myC(6).ParentCatalog = Cat1 myC(6).Name = "Цена" myC(6).Type = adCurrency Call myT.Columns.Append(myC(6))
'Создание ключа myK.Columns.Append (myC(1)) myK.RelatedTable = "myT" myK.Type = adKeyPrimary myK.Name = "Код_книги" Call myT.Keys.Append(myK)
'Присоединение таблицы myT.Name = "Книги" Call Cat1.Tables.Append(myT) End Sub
А теперь комментарии к этой процедуре:
В процедуре создаются, формируются, а затем добавляются к соответствующей коллекции объекты Table, Column и Key. Работа с ними ведется в полном соответствии со схемой, которую я приводил выше. Заметьте, я ввел массив объектов Column, число элементов которого определяется по числу полей, добавляемых в таблицу.
Для каждого поля таблицы я задавал лишь три свойства - имя, тип и ссылку на родительский каталог, оставляя значения остальных полей те, которые приняты по умолчанию. Для текстовых полей я не указывал и тип поля, поскольку это тип, принятый по умолчанию.
Замечу, что мои попытки указать тип поля в методе Append в момент присоединения поля к коллекции не увенчались успехом в данном контексте. В этом случае все поля создаваемой таблицы в Access имели один и тот же текстовый тип.
При формировании ключа - объекта myK я добавил к его коллекции Columns ключевое поле - уже созданный объект myC(1). Заметьте, добавление в коллекцию Columns объекта Key возможно только после того, как объект добавлен в коллекцию Columns объекта Table.
Далее для ключа были заданы его имя, тип и ссылка на родительский объект - таблицу myT, после чего мне осталось только присоединить объект к коллекции Keys.
Последним шагом было присоединение таблицы с именем "Книги", чьи свойства Tables и Keys уже сформированы, к коллекции Tables объекта Catalog, для которого установлено соединение с ранее созданной базой данных Access с именем NewDB.
Объект Catalog
Центральный объект модели ADOX, задает каталог базы данных. Его метод Create позволяет создать новый источник данных, а свойства позволяют добраться до элементов, определяющих схему базы данных. Не все Провайдеры поддерживают полный набор возможностей этого объекта, определяемых его свойствами и методами. Так что источник данных и его Провайдер определяют, допустимо ли для него использование того или иного свойства или метода объектов ADOX.
Объект Column
Коллекция Columns с элементами Column возвращается при вызове одноименного свойства объекта Table, в этом случае она задает поля таблицы. Но эта коллекция появляется и в других случаях. Напомню, ключи и индексы таблицы могут быть составными и состоять из нескольких полей, по этой причине объекты Key и Index также обладают свойством Columns, которое возвращает коллекции полей, задающих составной ключ или составной индекс. Так что объект Column может задавать поле таблицы, ключа или индекса. Этот объект является центральным объектом в мире, связанном с таблицами. Устроен он достаточно просто, у него нет ни методов, ни событий, - есть только свойства.
Объект Group
Группы облегчают управление правами доступа. Проще, быстрее и правильнее с содержательной точки зрения задать права группы, чем задавать их индивидуально для каждого пользователя. Хотя, конечно же, есть пользователи, для которых права задаются индивидуально. С объектной точки зрения объект Group, во многом, устроен аналогично объекту User, - у него нет метода ChangePassword, поскольку у групп нет паролей, а свойство Groups заменяет симметричное свойство Users. Таким образом, у объекта Group два свойства: Name, Users и два метода - GetPermissions, SetPermission.
Объект Index
Если таблица имеет первичный ключ, то она имеет и индекс, который автоматически создается по данному ключу. Однако индексы могут создаваться и независимо от создания ключевых полей. На индексы не накладывается жесткое условие уникальности значений индекса в таблице. Индексы служат для того, чтобы ускорить операции по поиску и сортировке данных таблицы. Платой за это является увеличение памяти, требуемой для индексов и увеличение времени, требуемое на перестроение индексов, когда записи в таблицу добавляются или удаляются. Для больших, редко обновляемых таблиц, к которым часто приходится обращаться для поиска данных, построение индексов может существенно ускорить работу с базой данных. Для больших, часто обновляемых таблиц задание индексов может приводить к потере эффективности. Поэтому разумное введение индексов требует, как правило, проведения специальных исследований и опыта работы у администратора базы данных. Объект Index задает индекс таблицы. Он создается с использованием конструктора New и добавляется в коллекцию Indexes методом Append, в соответствии с ранее приведенной схемой. Как и объекты Table и Key он имеет только свойства. Рассмотрим их:
Property Name As String. Свойство по умолчанию, задает имя индекса.
Property PrimaryKey As Boolean, Property Unique As Boolean, Property Clustered As Boolean. Булевы свойства, позволяющие определить, является ли индекс первичным ключом, возможным ключом с неповторяющимися значениями индекса, составным индексом, состоящим из нескольких полей.
Property Columns As Columns. Для составного индекса возвращает коллекцию полей, входящих в индекс.
Property IndexNulls As AllowNullsEnum. Позволяет указать, как обрабатываются значения Null, если они встречаются в поле индекса.
Property Properties As Properties. Возвращает коллекцию свойств, специфических для используемого Провайдера.
Объект Key
Напомню, ключом таблицы называется совокупность полей, однозначно идентифицирующая каждую запись таблицы. Если для идентификации записи требуется несколько полей, то такой ключ называется составным. Среди возможных ключей выделяется один, называемый первичным ключом (primary key). Access при построении таблицы, если не задается ее ключ, предлагает включить в число полей специальное поле типа "Счетчик". Это поле объявляется ключевым, а его значения строятся автоматически, чаще всего путем увеличения предыдущего значения счетчика на 1 при каждом добавлении новой записи, что и обеспечивает уникальность значений ключа. Однако, заметьте, среди типов полей, задаваемых перечислением DataTypeEnum, значения, соответствующего полю "Счетчик", нет. Таблицы в реляционных базах данных связываются между собой, за счет того, что они имеют общие ключевые поля. Поле в связанной таблице называется внешним ключом, если это поле является частью ключа другой таблицы. Таблица с первичным ключом называется основной или базисной, а таблица, содержащая внешний ключ, связанной или связующей. В предыдущих версиях Access все таблицы обязаны были иметь первичный ключ. В Access 2000 связующие таблицы могут и не иметь первичного ключа. Разумно, однако, задавать ключи для всех таблиц базы данных. Ключ, который не является первичным, имеет статус "уникальный" (Unique). Объект Key представляет первичный, внешний или уникальный ключ. Создается объект конструктором New, а добавляется в коллекцию методом Append, в соответствии с приведенной выше схемой. С объектной точки зрения объект Key устроен достаточно просто. Также как и многие другие объекты ADOX, он не имеет ни событий, ни методов, - только свойства. Их немного, - всего 6. Вот их описание:
Property Name As String. Задает имя ключа.
Property Type As KeyTypeEnum. Задает тип ключа. Возможные значения задаются константами перечисления: adKeyPrimary, adKeyForeign, adKeyUnique, определяющими, соответственно, первичный, внешний и уникальный ключ.
Property Columns As Columns. Возвращает коллекцию полей для составного ключа.
Property DeleteRule As RuleEnum, Property UpdateRule As RuleEnum. Эти два свойства определяют правила (процедуры), выполняемые при удалении или изменении первичного ключа.
Property RelatedTable As String. Задает таблицу, которой принадлежит ключ, что позволяет подняться по иерархии объектов.
Объект Procedure и коллекция Procedures
Объект Procedure представляет собой хранимую процедуру. Хранимые процедуры, текст которых, чаще всего, пишется на SQL, хранятся на сервере в оттранслированном виде, что ускоряет их выполнение. Еще одно немаловажное достоинство состоит в том, что облегчается задача программиста, вызывающего хранимую процедуру, когда он пишет свой код на VBA или VBScript, поскольку ему не нужно вникать в тонкости операторов SQL. Коллекция Procedures объекта Catalog включает в себя все хранимые процедуры базы данных. Как и у всех коллекций ADOX у нее три метода: Append, Delete, Refresh и два свойства - Item и Count. Тем не менее, есть особенность в создании объекта Procedure и добавлении его в коллекцию. Прежняя схема, используемая при создании объектов Table, Key, Index для него не применима. Этот объект не может быть создан с помощью конструктора New. Для того чтобы добавить в коллекцию новый объект Procedure, следует создать объект Command, задать текст процедуры, используя свойство CommandText, а затем применить метод Append коллекции Procedures. Заметьте, у этой коллекции метод Append имеет два параметра, - первый задает имя хранимой процедуры, второй объект Command, определяющий, по существу, саму процедуру. Используя объекты Procedure, Procedures и Command можно программно создать хранимую процедуру, модифицировать существующую процедуру или удалить процедуру из коллекции. Объект Procedure устроен очень просто, - у него всего 4 свойства:
Property Name As String. Задает имя объекта. Имеет статус "только для чтения", что объясняется особенностями создания этого объекта.
Property DateCreated As Variant, Property DateModified As Variant. Эти два свойства, также имеющие статус "только для чтения", задают дату создания и последней модификации процедуры.
Property Command As Variant. Задает объект Command из библиотеки ADODB. В предыдущей главе этот объект был достаточно подробно описан и приведено большое число примеров работы с ним. Свойство является центральным, поскольку именно объект Command определяет суть хранимой процедуры и позволяет создать процедуру.
Объект Record
Этот объект представляет отдельную запись набора Recordset. Поскольку, однако, объект Recordset не имеет свойства Records, возвращающее коллекцию записей, ни свойства Item, возвращающего отдельную запись, то явным способом получить этот объект из набора записей не удается. Некоторые Провайдеры возвращают объект Record в тех случаях, когда в результате запроса результатом является объект Recordset, состоящий из одной записи. Обычно создается этот объект методом Open, имеющим привычный для объектов ADO синтаксис: Sub Open([Source], [ActiveConnection], [Mode As ConnectModeEnum], [CreateOptions As RecordCreateOptionsEnum = adFailIfNotExists], [Options As RecordOpenOptionsEnum = adOpenRecordUnspecified], [UserName As String], [Password As String])
При открытии задается источник данных, соединение, устанавливаются параметры открытия и при необходимости задаются имя пользователя и пароль доступа к данным. Заметьте, источники могут быть различных типов. Объект Record хотя и подобен набору записей, состоящему из одной записи, но имеет все-таки отличающийся набор свойств и методов. Этот объект часто используют при доставке данных из Интернета, поскольку его значениями могут быть файл или каталог. Некоторые Провайдеры поддерживают объекты Record и Stream как альтернативу объекту Recordset или как дополнительные объекты, позволяющие манипулировать данными, поступающими от Провайдеров. В роли Провайдера часто выступает Microsoft OLE DB Provider for Internet Publishing. Этот Провайдер позволяет использовать объект Record для управления данными, такими как каталоги и папки файловой системы, а также сообщениями в системе электронной почты. Источником данных в этом случае выступает абсолютный или относительный адрес URL, сочетаемый с объектом Connection. Этот объект с успехом может использоваться при работе с данными, имеющими иерархическую структуру. С одной стороны его родителем может быть объект Recordset. С другой стороны, объект Recordset может быть его потомком, поскольку метод GetChildren объекта Record возвращает в качестве результата объект Recordset. Чуть позже я приведу пример такого взаимодействия этих двух объектов. Большая часть свойств объекта Record совпадает со свойствами объекта Recordset. К таковым относятся свойства: ActiveConnection, Source, Fields, Properties, State. То, что эти два объекта имеют свойство Fields, возвращающее коллекцию полей записей, более всего роднит эти объекты. Еще одно свойство Mode, имеющее значениями константы из перечисления ConnectModeEnum, является общим с объектом Connection. Особых свойств у этого объекта два:
RecordType задает тип объекта, его значениями являются константы из перечисления RecordTypeEnum: adCollectionRecord, adSimpleRecord, adStructDoc, имена которых достаточно точно отражают смысл типа объекта.
ParentURL определяет URL-адрес родителя объекта Record.
Методов у объекта Record немного. О центральном методе Open, позволяющем создать объект я уже сказал. Кроме этого есть типичные для всех объектов методы Close и Cancel. Три метода CopyRecord, MoveRecord и DeleteRecord выполняют типичные операции над этим объектом. О специфическом методе GetChildren скажу чуть подробнее. Его синтаксис:
Function GetChildren() As Recordset
Записи возвращаемого объекта Recordset являются потомками текущего объекта Record. Например, потомками объекта Record, представляющего собой каталог, могут быть файлы и подкаталоги, содержащиеся внутри родительского каталога.
Рассмотрим теперь пример, в котором в роли Провайдера выступает Internet Publishing Provider, а участниками будут объекты Record, Recordset и Stream. Я поочередно создам два объекта Record, в первый раз для чтения текстового файла и передачи этих данных объекту Stream. Во втором случае объект Record будет представлять каталог, а созданный на его основе объект Recordset будет содержать имена файлов этого каталога. Вот процедура, решающая эту задачу:
Public Sub CreateRecord() 'Создание и работа с объектом Record Dim FirstRec As New Record Dim fld As Field Dim strText As String 'Create Connection with Internet Publishing Provider If Con1.State = adStateOpen Then Con1.Close 'закрыть соединение Con1.Open "Provider = MSDAIPP.DSO; " & _ "Data Source = http://serverva/proba2_2/testtext.txt" 'Объекту Record передается текстовый файл FirstRec.Open ActiveConnection:=Con1 'Поля объекта Record Debug.Print "Число полей:", FirstRec.Fields.Count For Each fld In FirstRec.Fields Debug.Print "Имя поля: ", fld.Name, _ "Значение поля:", fld.Value, "Тип:", fld.Type Next fld
'Создание объекта Stream на основе объекта Record If Strm1.State = adStateOpen Then Strm1.Close Strm1.Open Source:=FirstRec, Mode:=adModeRead, _ Options:=adOpenStreamFromRecord strText = Strm1.ReadText(100) Debug.Print "Текстовый файл:", strText Con1.Close 'Новое соединение и открытие объекта Record 'Объекту Record передается каталог Con1.Open "Provider = MSDAIPP.DSO; " & _ "Data Source = http://serverva/" FirstRec.Open ActiveConnection:=Con1, _ Options:=adOpenSource, Mode:=adModeRead 'Поля объекта Record Debug.Print "Число полей:", FirstRec.Fields.Count For Each fld In FirstRec.Fields Debug.Print "Имя поля: ", fld.Name, _ "Значение поля:", fld.Value, "Тип:", fld.Type Next fld 'Создание объекта Recordset Set Rst1 = FirstRec.GetChildren Debug.Print Rst1.ActiveConnection Debug.Print Rst1.RecordCount Rst1.MoveFirst Do While Not Rst1.EOF 'Обработка текущей записи Debug.Print Rst1(2).Name, Rst1(2).Value Rst1.MoveNext Loop Con1.Close
End Sub
Приведу комментарии к этой программе:
При установлении соединения с Провайдером в качестве источника данных я указал текстовый файл, находящийся в виртуальном каталоге Proba2_2. Этот каталог установлен под Internet Information Server на моем компьютере. Соответствующие строки программного текста подсвечены.
Создается объект FirstRec класса Record методом Open, которому в качестве параметра передается имя открытого соединения. В этот момент данные из текстового файла передаются объекту FirstRec.
Я не стал выводить информацию о всех свойствах открытого объекта, ограничившись лишь наиболее важным свойством Fields. Полей у этого объекта в данном контексте достаточно много - 26. Приведу имена и значения пяти полей:
В данном контексте большая часть полей, но не все, определяет характеристики ресурса.
Поскольку созданный объект FirstRec задает теперь текстовый файл, то он может в свою очередь послужить источником данных для объекта Stream, который создается, как обычно, методом Open.
Вызов метода ReadText глобального объекта Strm1 класса Stream позволяет прочесть нужную порцию символов в обычную строку и распечатать ее, что позволяет убедиться в правильности передачи данных.
На следующем шаге объект FirstRec закрывается и заново открывается, но теперь у него другой источник данных и его значением становится каталог.
Число полей в данном контексте уменьшилось до 18, все они определяют характеристики ресурса. Приведу имена и значения тех же полей в этом новом контексте:
В сравнении с предыдущим случаем не для всех полей определены значения, в частности не задан класс контента.
Данный объект, представляющий каталог, может использоваться для построения объекта Recordset, записи которого будут содержать сведения о подкаталогах и файлах, хранимых в каталоге. Для создания объекта Recordset используется метод GetChildren, вызываемый объектом FirstRec.
Для каждой записи из созданного набора я вывожу на печать имя и значение второго поля. Приведу лишь первые пять строчек:
На этом я закончу описание примера, в котором успешно взаимодействовали объекты Record, Stream и Recordset. Знакомство с объектом Stream уже состоялось, но давайте, хотя бы вкратце познакомимся с его свойствами и методами.
Объект Stream
Этот объект представляет двоичный поток данных или текст. Методы и свойства этого объекта позволяют выполнять операции над этим потоком, позволяя читать, писать, копировать и сохранять данные потока. Объекты Stream обычно используются при работе с данными, сохраненными в XML-формате, при работе с текстовыми файлами и сообщениями электронной почты, во всех случаях, когда данные рассматриваются как поток байтов. Объект Stream может быть получен тремя способами:
Заданием URL-адреса, указывающего на объект, обычно файл, содержащий двоичные данные или текст. Этот объект может быть простым документом, объектом Record, представляющим структурированный документ, или папкой.
Заданием потока, ассоциированного с объектом Record.
Быть созданным непосредственно в приложении, не будучи связанным ни с каким источником данных.
Объект Table
Этот объект определяет таблицу базы данных. Является элементом коллекции Tables, вложенной в объект Catalog. Создается конструктором New и добавляется в коллекцию Tables методом Append. Вот типичная схема программного создания новой таблицы: Dim NewTable As New ADOX.Table 'Формирование объекта Table … Call Cat1.Tables.Append(NewTable)
Чуть позже я приведу полный пример программного создания объекта Table, а пока займемся рассмотрением его свойств, методов и событий. Задача облегчается тем, что у объекта Table нет методов и событий, - есть только свойства, рассмотрением которых сейчас и займемся.
Объект User
Объект определяет учетную запись пользователя, позволяет задать имя пользователя, пароль, права доступа к объектам базы данных и группы, в которые входит этот пользователь. У этого объекта два свойства:
Property Name As String. Задает имя пользователя. Является свойством по умолчанию.
Property Groups As Groups. Возвращает одноименную коллекцию, содержащую все группы, в которые входит данный пользователь.
Пароль пользователя и разрешения на право доступа к тем или иным объектам базы данных задаются методами объекта User. Методов всего три:
Sub ChangePassword(OldPassword As String, NewPassword As String). Чтобы задать новый пароль, нужно знать и старый пароль. В случае, когда пароль задается в первый раз, старым паролем является пустая строка.
Function GetPermissions(Name, ObjectType As ObjectTypeEnum, [ObjectTypeId]) As RightsEnum. Метод (функция) возвращает разрешения на то, что может делать пользователь с объектом базы данных, чье имя задается параметром Name, а тип - параметром ObjectType. Третий параметр указывается лишь в тех исключительных ситуациях, когда речь идет об объектах, специфических для Провайдера. Права пользователя на объект задаются константами из перечисления RightsEnum. Возвращаемое значение может быть суммой отдельных констант и представляет маску, задающую отдельные разрешения. Различных констант в перечислении достаточно много, приведу лишь некоторые из них: adRightCreate, adRightDelete, adRightMaximumAllowed, adRightNone.
Sub SetPermissions(Name, ObjectType As ObjectTypeEnum, Action As ActionEnum, Rights As RightsEnum, [Inherit As InheritTypeEnum = adInheritNone], [ObjectTypeId]). Метод позволяет установить разрешения на возможные действия с элементом базы, чье имя задано параметром Name, а тип - параметром ObjectType. Параметр Rights задает маску, определяющую право на те или иные действия, значением параметра является сумма констант из перечисления RightsEnum. Параметр Action задает действия, выполняемые при установке разрешений. Параметр Inherit указывает, как объекты наследуют разрешения.
Объект View и коллекция Views
Объекты View задают так называемые представления. Они подобны объектам Procedure, хранятся на сервере в виде оттранслированных запросов и задают отфильтрованное множество записей. Представления являются элементами таких баз данных как Microsoft SQL Server , а, следовательно, и MSDE. Поскольку с объектной точки зрения объект View имеет те же свойства, что и объект Procedure и соответствующие коллекции устроены одинаково, то я не буду задерживаться на более подробном описании этих объектов.
Объекты ADOX
Объекты этой группы, как уже было сказано, играют важную роль, позволяя работать с метаданными баз данных. С их помощью можно программно создать новую базу данных, создать или модифицировать элементы существующей базы данных, создать новые или модифицировать существующие таблицы, запросы, хранимые процедуры. Кроме того, эти объекты предназначены для работы с пользователями, группами пользователей, задания их прав доступа к данным, обеспечивая, тем самым, нужный уровень безопасности. Рассмотрим объектную модель ADOX и начнем со схемы, отражающей взаимосвязи между объектами модели:
Рис. 6.7. Объектная модель ADOX На схеме не отражен тот факт, что объекты Table, Index, Column имеют стандартную коллекцию Properties с элементами Property. Заметьте, в этой модели есть центральный объект Catalog, в который и встроены все основные коллекции: Tables, Procedures, Views, Groups, Users. Эти коллекции определяют различные метаэлементы базы данных. Давайте подробно рассмотрим свойства и методы объектов модели.
Объекты User, Group и их коллекции
Для того чтобы обеспечить защиту баз данных от неавторизованного доступа, для того чтобы разным пользователям предоставить разные права доступа к тем или иным объектам базы данных - таблицам, запросам, полям таблицы, одним из основных используемых механизмов является введение учетных записей для пользователей и групп пользователей базы данных. На объектном уровне защита баз данных поддерживается объектами User, Group и их коллекциями. Свойства Users и Groups объекта Catalog возвращают одноименные коллекции всех пользователей и всех групп, связанных с базой данных - объектом Catalog. Но поскольку каждая группа содержит одного или нескольких пользователей, то свойством Users обладает и объект Group, для него свойство возвращает всех пользователей, входящих в группу. Симметрично, свойством Groups обладает объект User, для него свойство возвращает все группы, в которые входит данный пользователь. Коллекции Users и Groups устроены, как и все коллекции ADOX, у них три метода - Append, Delete, Refresh и два свойства: Item и Count. Создаются объекты и присоединяются к коллекции также согласно общей схеме, - при создании используется конструктор New, для присоединения - метод Append. Замечу, что, наконец-то, хотя бы в ADOX достигнута желаемая общность в организации коллекций.
Особенности работы с хранимыми процедурами в Access
В базе данных Access нет понятия хранимых процедур, их роль играют хранимые запросы. Было бы хорошо, если бы Провайдер Microsoft Jet при создании объектов коллекции Procedures создавал вместо этого хранимые запросы. Однако этого не происходит, - запросы не создаются. Поэтому, казалось бы, при работе с базой данных Access создавать программно процедуры невозможно и, главное, бесполезно. Тем не менее, при попытке создать коллекцию Procedures никаких ошибок не возникает и, по крайней мере, одна процедура создается и с ней можно впоследствии работать, вызывая ее на исполнение. Я приведу сейчас пример, в котором демонстрируется сам способ создания коллекции Procedures. Этот способ не зависит от Провайдера и базы данных, он может с успехом использоваться при работе с Access MSDE или, например, с Microsoft SQL Server. Но, главная суть примера в том, чтобы показать недокументированную возможность программной работы с хранимой процедурой в Access. Как всегда, приведу вначале текст процедуры: Public Sub CreateQuery() 'Создание хранимых процедур Dim myCmd1 As New Command, myCmd2 As New Command 'Установить соединение с базой NewDB CreateConnection Cat1.ActiveConnection = Con1 'SQL-запросы, представляющие тексты хранимых процедур myCmd1.CommandText = "Select [Книги.Автор],[Книги.Название_книги]" & _ "From [Книги]" & _ "WHERE ((([Книги.Год_издания])= 1999))" & _ "ORDER BY [Книги.Автор];" 'Запрос с параметром myCmd2.CommandText = "Parameters [Avtor] Text(50);" & _ "Select [Книги.Автор],[Книги.Название_книги]" & _ "From [Книги]" & _ "WHERE ((([Книги.Автор])= [Avtor]));" 'Удаление процедур Dim i As Integer For i = 0 To Cat1.Procedures.Count - 1 Cat1.Procedures.Delete (i) Next i 'Добавление процедур Debug.Print Cat1.Procedures.Count Call Cat1.Procedures.Append("Books1999", myCmd1) Debug.Print Cat1.Procedures.Count Call Cat1.Procedures.Append("BooksOfAuthor", myCmd2) Debug.Print Cat1.Procedures.Count
End Sub
Сама процедура и то, как она выполняется, нуждается в подробных комментариях:
Поскольку целью работы, которую я поставил перед собой, являлось создание двух хранимых процедур, то в процедуре создаются два объекта Command.
Свойство CommandText этих объектов позволяет задать текст команд, представляющих в данном случае SQL-запросы. Второй из этих запросов представляет запрос с параметром.
Успешно выполняется метод Append коллекции Procedures, которому в качестве одного из параметров передаются созданные объекты Command. По завершении работы процедуры создается впечатление, что коллекция Procedures с двумя элементами успешно создана. Однако это не совсем так.
Прежде всего, заметьте, что никакие запросы в базе данных Access не появляются. Коллекция процедур действительно создается, но в ней присутствует только один элемент. При добавлении нового элемента он записывается на место ранее существовавшего. Так что свойство Count в конце работы этой процедуры будет возвращать число 1, а сама коллекция будет хранить второй параметрический запрос.
Досадной ошибкой, усугубляющей ситуацию, является то, что имена записываемых процедур сохраняются полностью, не забивая друг друга. По этой причине при повторном запуске процедуры, несмотря на то, что все процедуры из коллекции удаляются перед их созданием, возникнет ошибка с выдачей сообщения о том, что есть хранимая процедура с именем "Books1999", хотя коллекция и пуста.
Не следует особенно расстраиваться, что хранимые процедуры не работают корректно в Access, - они и не должны работать. Можно, однако, использовать тот факт, что с одной процедурой все-таки работать разрешается. Вот пример работы, демонстрирующий работу с сохраненной в предыдущем примере процедурой с именем BooksOfAuthor, имеющей имя автора в качестве параметра запроса:
Public Sub WorkWithProcs() 'работа с хранимыми процедурами Dim myCmd As Command 'Установить соединение с базой NewDB CreateConnection Cat1.ActiveConnection = Con1 Set myCmd = Cat1.Procedures("BooksOfAuthor").Command Set Rst1 = myCmd.Execute(, "Пушкин") With Rst1 .MoveFirst Do While Not .EOF Debug.Print .Fields(0), .Fields(1) .MoveNext Loop End With End Sub
В результате работы этой процедуры на печать были выданы все книги, написанные Пушкиным, записи о которых хранились в базе данных.
End Sub
Сама процедура и то, как она выполняется, нуждается в подробных комментариях:
Поскольку целью работы, которую я поставил перед собой, являлось создание двух хранимых процедур, то в процедуре создаются два объекта Command.
Свойство CommandText этих объектов позволяет задать текст команд, представляющих в данном случае SQL-запросы. Второй из этих запросов представляет запрос с параметром.
Успешно выполняется метод Append коллекции Procedures, которому в качестве одного из параметров передаются созданные объекты Command. По завершении работы процедуры создается впечатление, что коллекция Procedures с двумя элементами успешно создана. Однако это не совсем так.
Прежде всего, заметьте, что никакие запросы в базе данных Access не появляются. Коллекция процедур действительно создается, но в ней присутствует только один элемент. При добавлении нового элемента он записывается на место ранее существовавшего. Так что свойство Count в конце работы этой процедуры будет возвращать число 1, а сама коллекция будет хранить второй параметрический запрос.
Досадной ошибкой, усугубляющей ситуацию, является то, что имена записываемых процедур сохраняются полностью, не забивая друг друга. По этой причине при повторном запуске процедуры, несмотря на то, что все процедуры из коллекции удаляются перед их созданием, возникнет ошибка с выдачей сообщения о том, что есть хранимая процедура с именем "Books1999", хотя коллекция и пуста.
Не следует особенно расстраиваться, что хранимые процедуры не работают корректно в Access, - они и не должны работать. Можно, однако, использовать тот факт, что с одной процедурой все-таки работать разрешается. Вот пример работы, демонстрирующий работу с сохраненной в предыдущем примере процедурой с именем BooksOfAuthor, имеющей имя автора в качестве параметра запроса:
Public Sub WorkWithProcs() 'работа с хранимыми процедурами Dim myCmd As Command 'Установить соединение с базой NewDB CreateConnection Cat1.ActiveConnection = Con1 Set myCmd = Cat1.Procedures("BooksOfAuthor").Command Set Rst1 = myCmd.Execute(, "Пушкин") With Rst1 .MoveFirst Do While Not .EOF Debug.Print .Fields(0), .Fields(1) .MoveNext Loop End With End Sub
В результате работы этой процедуры на печать были выданы все книги, написанные Пушкиным, записи о которых хранились в базе данных.
Построение обработчиков событий и обработка ошибок
Давайте разберемся с деталями построения обработчиков событий объектов ADO. Прежде всего, рассмотрим, как они создаются, в какой модуль проекта их следует поместить. Хотя объекты Recordset и Connection обладают событиями, но появляются они как объекты без событий. Необходимо предпринять обычные для VBA в таких ситуациях действия, чтобы создать объекты с событиями и написать процедуры, обрабатывающие события, - обработчики событий. В первом томе и других книгах серии "Офисное программирование" я подробно рассказывал о том, как это делается. Напомню, что для решения этой задачи нужно выполнить три шага:
Создать на VBA собственный класс с именем, например MyEventsADO, в котором описать вложенные объекты - Connection WithEvents и Recordset WithEvents.
Создать обработчики нужных событий для данных объектов в классе MyEventsADO. Заметьте, после объявления переменных With Events в этом классе появится возможность написания обработчиков событий, следуя обычной технологии.
В подходящем месте, например в уже существующем модуле TestingADO, создать экземпляры класса MyEventsADO и связать их с глобальными или локальными объектами Rst1 и Con1, задающими набор записей и соединение.
Чтобы сделать дальнейшее описание конкретным, давайте рассмотрим создание обработчика события WillChangeField объекта Recordset, которое, напомню, появляется при попытках изменить значение поля в записи набора, но перед тем, как это изменение реально произойдет. Вот как выглядит модуль класса MyEventsADO, содержащий обработчик этого события: 'Класс MyEventsADO, определяющий события ADO Public WithEvents EvRst As ADODB.Recordset Public WithEvents EvCon As ADODB.Connection
Private Sub EvRst_WillChangeField(ByVal cFields As Long, _ ByVal Fields As Variant, adStatus As ADODB.EventStatusEnum, _ ByVal pRecordset As ADODB.Recordset) 'Печать параметров обработчика события MsgBox "Номер поля = " & cFields & vbCrLf & _ "Значение поля = " & Fields(cField) & vbCrLf & _ "Позиция записи в наборе = " & _ pRecordset.AbsolutePosition & vbCrLf & _ "Статус выполняемой операции = " & adStatus 'Изменение статуса приведет к появлению ошибки 'при попытке изменить значение поля, обработка которой 'позволит отменить операцию. If IsNumeric(Fields(cField)) Then If Fields(cField) > 75 Then adStatus = adStatusCancel End If End Sub
Первым делом я включил в обработчике события WillChangeField печать всех его параметров на входе. Обратите внимание, обработчику события передается указатель на сам объект Recordset, поэтому доступна практически любая информация, связанная с этим объектом. Я, например, использовал его, чтобы вывести на печать позицию записи в наборе, поля которой обновляются. Во второй части обработчика я изменяю значение статуса при выполнении некоторого условия на обновляемое поле. Делаю это для того чтобы реализовать возможность отмены изменения значения, когда в результате анализа, проведенного в обработчике, принимается решение о нежелательности выполнения изменений. Заметьте, для этого свойству статус, которое имело значение adStatusOk, уведомляющее о том, что изменения возможны, я присваиваю значение adStausCancel. В этом случае при выходе из обработчика возникнет ошибка в операторе, производящем изменения. Обработав соответствующим образом эту ошибку, я смогу выполнить поставленную задачу.
Давайте теперь рассмотрим процедуру, в которой происходит обновление полей записей набора, чьи действия будут вызывать появление события WillChangeField. Эта процедура изменяет цену у некоторых книг из таблицы "Книги" нашей тестовой базы данных и добавляет новую запись в эту таблицу. При всех этих изменениях начнут работать события:
Public Sub CreateEvents() 'Объект с событиями Dim imEvRst As New MyEventsADO 'добавление и изменение записей базы данных Dim recExist As Boolean ' Связывание объекта с событиями Set imEvRst.EvRst = Rst1 'Создать соединение CreateConnection 'Создать команду 'задание свойств объекта Command Cmd1.ActiveConnection = Con1 Cmd1.CommandText = "Select * From [Книги]" Cmd1.CommandType = adCmdText 'Открытие обновляемого объекта Recordset With Rst1 .Open Source:=Cmd1, CursorType:=adOpenDynamic, _ LockType:=adLockOptimistic 'Изменение записей recExist = False .MoveFirst Do While Not .EOF 'Обработка текущей записи On Error Resume Next If !Название = "Офисное программирование" Then recExist = True If ![Год издания] < 2000 And !Цена < 100 Then MsgBox "Старая цена =" & Str(!Цена) !Цена = !Цена * 2 .Update End If .MoveNext Loop If Not recExist Then .AddNew !Автор = "Владимир Биллиг" !Название = "Офисное программмирование" ![Год издания] = 2001 ![Число страниц] = 599 !Цена = 150 .Update End If End With End Sub
Обратите внимание, первым делом я объявляю объект imEvRst созданного класса MyEventsADO и связываю его с глобальным объектом Rst1, после чего последний начнет реагировать на события. Сколько раз будет появляться событие WillChangeField? Очевидно, при каждом выполнении оператора !Цена = !Цена * 2, изменяющего значение соответствующего поля записи. Вот как выглядят окошки функции MsgBox, одно из которых открывается перед выполнением этого оператора, а второе в обработчике события в момент выполнения оператора:
Рис. 6.1. Окна, открываемые перед изменением поля и в момент изменения
Соответствующие сообщения появляются и при создании новой записи, причем столько раз, сколько полей имеет запись.
Важную роль в этой процедуре играет и оператор On Error Resume Next. Всякий раз, когда в обработчике события изменяется значение переменной adStatus, возникнет ошибка при выполнении оператора, изменяющего значение поля и послужившего причиной возникновения события. Оператор OnError позволяет в этом случае обойти выполнение оператора и, тем самым, избежать изменения значения, что и хотелось. Взгляните, как выглядит окно, уведомляющее об ошибке, появляющееся, если в процедуре закомментировать оператор On Error:
Рис. 6.2. Окно, уведомляющее о возникновении ошибки при изменении значения
Хочу теперь рассмотреть пример построения обработчика события, возникающего после завершения операции. Пример хочу построить такой, чтобы при выполнении операции Провайдер столкнулся с трудностями и сформировал объекты Error, а обработчик события соответственно обрабатывал эти ошибки. Но прежде чем перейти к рассмотрению этого примера, есть смысл более подробно познакомиться с объектом Error - его свойствами и методами.
Первым делом я включил в обработчике события WillChangeField печать всех его параметров на входе. Обратите внимание, обработчику события передается указатель на сам объект Recordset, поэтому доступна практически любая информация, связанная с этим объектом. Я, например, использовал его, чтобы вывести на печать позицию записи в наборе, поля которой обновляются. Во второй части обработчика я изменяю значение статуса при выполнении некоторого условия на обновляемое поле. Делаю это для того чтобы реализовать возможность отмены изменения значения, когда в результате анализа, проведенного в обработчике, принимается решение о нежелательности выполнения изменений. Заметьте, для этого свойству статус, которое имело значение adStatusOk, уведомляющее о том, что изменения возможны, я присваиваю значение adStausCancel. В этом случае при выходе из обработчика возникнет ошибка в операторе, производящем изменения. Обработав соответствующим образом эту ошибку, я смогу выполнить поставленную задачу.
Давайте теперь рассмотрим процедуру, в которой происходит обновление полей записей набора, чьи действия будут вызывать появление события WillChangeField. Эта процедура изменяет цену у некоторых книг из таблицы "Книги" нашей тестовой базы данных и добавляет новую запись в эту таблицу. При всех этих изменениях начнут работать события:
Public Sub CreateEvents() 'Объект с событиями Dim imEvRst As New MyEventsADO 'добавление и изменение записей базы данных Dim recExist As Boolean ' Связывание объекта с событиями Set imEvRst.EvRst = Rst1 'Создать соединение CreateConnection 'Создать команду 'задание свойств объекта Command Cmd1.ActiveConnection = Con1 Cmd1.CommandText = "Select * From [Книги]" Cmd1.CommandType = adCmdText 'Открытие обновляемого объекта Recordset With Rst1 .Open Source:=Cmd1, CursorType:=adOpenDynamic, _ LockType:=adLockOptimistic 'Изменение записей recExist = False .MoveFirst Do While Not .EOF 'Обработка текущей записи On Error Resume Next If !Название = "Офисное программирование" Then recExist = True If ![Год издания] < 2000 And !Цена < 100 Then MsgBox "Старая цена =" & Str(!Цена) !Цена = !Цена * 2 .Update End If .MoveNext Loop If Not recExist Then .AddNew !Автор = "Владимир Биллиг" !Название = "Офисное программмирование" ![Год издания] = 2001 ![Число страниц] = 599 !Цена = 150 .Update End If End With End Sub
Обратите внимание, первым делом я объявляю объект imEvRst созданного класса MyEventsADO и связываю его с глобальным объектом Rst1, после чего последний начнет реагировать на события. Сколько раз будет появляться событие WillChangeField? Очевидно, при каждом выполнении оператора !Цена = !Цена * 2, изменяющего значение соответствующего поля записи. Вот как выглядят окошки функции MsgBox, одно из которых открывается перед выполнением этого оператора, а второе в обработчике события в момент выполнения оператора:
Рис. 6.1. Окна, открываемые перед изменением поля и в момент изменения
Соответствующие сообщения появляются и при создании новой записи, причем столько раз, сколько полей имеет запись.
Важную роль в этой процедуре играет и оператор On Error Resume Next. Всякий раз, когда в обработчике события изменяется значение переменной adStatus, возникнет ошибка при выполнении оператора, изменяющего значение поля и послужившего причиной возникновения события. Оператор OnError позволяет в этом случае обойти выполнение оператора и, тем самым, избежать изменения значения, что и хотелось. Взгляните, как выглядит окно, уведомляющее об ошибке, появляющееся, если в процедуре закомментировать оператор On Error:
Рис. 6.2. Окно, уведомляющее о возникновении ошибки при изменении значения
Хочу теперь рассмотреть пример построения обработчика события, возникающего после завершения операции. Пример хочу построить такой, чтобы при выполнении операции Провайдер столкнулся с трудностями и сформировал объекты Error, а обработчик события соответственно обрабатывал эти ошибки. Но прежде чем перейти к рассмотрению этого примера, есть смысл более подробно познакомиться с объектом Error - его свойствами и методами.
Пример программной работы в Access с пользователями и группами
База данных Microsoft Jet позволяет устанавливать защиту с использованием механизма учетных записей пользователей и групп пользователей. По умолчанию, когда создается новая база данных, автоматически создаются две типичные группы пользователей - Admins и Users. Первая из них включает администраторов базы, обладающих максимальными правами, другая - "обычных" пользователей. В специальной системной базе данных Access хранится информация о группах, о пользователях, их именах и паролях. Эта база данных хранится в файле с именем System и расширением - mdw. Обычный путь к этому файлу - "c:\Program Files\Microsoft Office\Offce\system.mdw". Фактический путь можно посмотреть и при необходимости изменить в реестре, где он находится в разделе: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Office\9.0\Access\Jet\4.0\Engines Параметр, задающий путь, имеет имя SystemDB. В нижеследующем примере я ввел в дополнение к существующим группам еще две группы - читателей и писателей - с именами, соответственно, Readers и Writers и четырех новых пользователей, входящих по-разному в эти группы. Группам заданы различные права на доступ к таблицам базы данных. Но, прежде чем давать другие пояснения приведу соответствующий программный код: Public Sub CreateUsersАndGroups() 'Cоздание пользователей и групп Dim myUs(1 To 4) As New User 'учетные записи пользователей Dim myGr(1 To 2) As New Group 'учетные записи групп 'Установить соединение с базой NewDB 'и системной базой данных System.mdw CreateConnectionSystemDB Cat1.ActiveConnection = Con1 'Пользователи myUs(1).Name = "Vladimir" myUs(1).ChangePassword "", "Old111" Cat1.Users.Append myUs(1)
myUs(2).Name = "Nina" Call myUs(2).ChangePassword("", "Best111") Call Cat1.Users.Append(myUs(2))
myUs(3).Name = "Ilya" Call myUs(3).ChangePassword("", "Prim111") Call Cat1.Users.Append(myUs(3))
myUs(4).Name = "Yuly" Call myUs(4).ChangePassword("", "Clev111") Call Cat1.Users.Append(myUs(4))
'Группы myGr(1).Name = "Readers" Call Cat1.Groups.Append(myGr(1)) myGr(1).Users.Append (myUs(1)) myGr(1).Users.Append (myUs(2)) myGr(1).Users.Append (myUs(3)) myGr(1).Users.Append (myUs(4))
myGr(2).Name = "Writers" Call Cat1.Groups.Append(myGr(2)) myGr(2).Users.Append (myUs(1))
'Установление прав Call myGr(1).SetPermissions("Книги", _ adPermObjTable, adAccessSet, adRightRead, adInheritBoth) Call myGr(1).SetPermissions("Заказчики", _ adPermObjTable, adAccessSet, adRightRead, adInheritBoth)
Call myGr(2).SetPermissions("Книги", _ adPermObjTable, adAccessSet, adRightFull, adInheritBoth) Call myGr(2).SetPermissions("Заказчики", _ adPermObjTable, adAccessSet, adRightFull, adInheritBoth)
Call myUs(3).SetPermissions("Заказчики", _ adPermObjTable, adAccessSet, adRightMaximumAllowed, adInheritBoth) End Sub
Прежде всего, хочу обратить внимание на главную особенность работы с объектами User, Group и их коллекциями, когда речь идет о Провайдере Microsoft Jet. При установлении соединения необходимо связаться не только с самой базой данных, но и с системной базой данных. По этой причине вызывается специальная процедура, устанавливающая такое соединение: Public Sub CreateConnectionSystemDB() 'Создание соединения с базой данных Access - NewDB ' и системной базой данных - System.mdw Dim strConnStr As String If Con1.State = adStateOpen Then Con1.Close 'закрыть соединение
Con1.Provider = "Microsoft.jet.oledb.4.0" Con1.ConnectionString = _ "Data Source=c:\!O2000\DsCd\Ch16\NewDB.mdb;" & _ "Jet OLEDB:System database =" & _ "c:\Program Files\Microsoft Office\Office\system.mdw" 'Открытие соединения Con1.Open End Sub
Заметьте, я задаю специфическое для Провайдера свойство "Jet OLEDB:System database" непосредственно в строке соединения, хотя мог бы это сделать, используя коллекцию Properties. Возвращаясь к процедуре CreateUsersAndGroups, приведу еще несколько комментариев к ее работе:
Создание объектов User и Group, присоединение их к коллекциям идет по традиционной схеме.
Лишь после того, как созданы пользователи и группы, создаются коллекции Users для каждой группы. Заметьте, при этом автоматически будут созданы коллекции Groups для каждого объекта User.
На последнем шаге я задаю различные права для каждой группы на доступ к таблицам "Книги" и "Заказчики", а также индивидуальные права на работу с таблицей "Заказчики" для одного из пользователей с именем "Ilya".
В заключение пара экранных снимков. Взгляните, как выглядит результаты одного из запросов к системной базе данных, после выполнения процедуры CreateUsersAndGroups:
Рис. 6.11. Результаты запроса к системной базе данных Access
Рис. 6.12. Показ прав доступа к элементам базы данных Access Конечно, все, что я сделал программно, можно сделать и руками, выбрав пункт меню "Сервис | Защита" и открыв соответствующее окно, например, окно разрешений, показанное на рис. 6.12.
'Группы myGr(1).Name = "Readers" Call Cat1.Groups.Append(myGr(1)) myGr(1).Users.Append (myUs(1)) myGr(1).Users.Append (myUs(2)) myGr(1).Users.Append (myUs(3)) myGr(1).Users.Append (myUs(4))
myGr(2).Name = "Writers" Call Cat1.Groups.Append(myGr(2)) myGr(2).Users.Append (myUs(1))
'Установление прав Call myGr(1).SetPermissions("Книги", _ adPermObjTable, adAccessSet, adRightRead, adInheritBoth) Call myGr(1).SetPermissions("Заказчики", _ adPermObjTable, adAccessSet, adRightRead, adInheritBoth)
Call myGr(2).SetPermissions("Книги", _ adPermObjTable, adAccessSet, adRightFull, adInheritBoth) Call myGr(2).SetPermissions("Заказчики", _ adPermObjTable, adAccessSet, adRightFull, adInheritBoth)
Call myUs(3).SetPermissions("Заказчики", _ adPermObjTable, adAccessSet, adRightMaximumAllowed, adInheritBoth) End Sub
Прежде всего, хочу обратить внимание на главную особенность работы с объектами User, Group и их коллекциями, когда речь идет о Провайдере Microsoft Jet. При установлении соединения необходимо связаться не только с самой базой данных, но и с системной базой данных. По этой причине вызывается специальная процедура, устанавливающая такое соединение: Public Sub CreateConnectionSystemDB() 'Создание соединения с базой данных Access - NewDB ' и системной базой данных - System.mdw Dim strConnStr As String If Con1.State = adStateOpen Then Con1.Close 'закрыть соединение
Con1.Provider = "Microsoft.jet.oledb.4.0" Con1.ConnectionString = _ "Data Source=c:\!O2000\DsCd\Ch16\NewDB.mdb;" & _ "Jet OLEDB:System database =" & _ "c:\Program Files\Microsoft Office\Office\system.mdw" 'Открытие соединения Con1.Open End Sub
Заметьте, я задаю специфическое для Провайдера свойство "Jet OLEDB:System database" непосредственно в строке соединения, хотя мог бы это сделать, используя коллекцию Properties. Возвращаясь к процедуре CreateUsersAndGroups, приведу еще несколько комментариев к ее работе:
Создание объектов User и Group, присоединение их к коллекциям идет по традиционной схеме.
Лишь после того, как созданы пользователи и группы, создаются коллекции Users для каждой группы. Заметьте, при этом автоматически будут созданы коллекции Groups для каждого объекта User.
На последнем шаге я задаю различные права для каждой группы на доступ к таблицам "Книги" и "Заказчики", а также индивидуальные права на работу с таблицей "Заказчики" для одного из пользователей с именем "Ilya".
В заключение пара экранных снимков. Взгляните, как выглядит результаты одного из запросов к системной базе данных, после выполнения процедуры CreateUsersAndGroups:
Рис. 6.11. Результаты запроса к системной базе данных Access
Рис. 6.12. Показ прав доступа к элементам базы данных Access Конечно, все, что я сделал программно, можно сделать и руками, выбрав пункт меню "Сервис | Защита" и открыв соответствующее окно, например, окно разрешений, показанное на рис. 6.12.
Пример работы с объектами ADO при создании Web-документов
Последний пример в этой главе я хочу посвятить хотя бы беглому знакомству использования объектов ADO в сценариях, написанных на VBScript. Я рассмотрю пример, представляющий фрагмент создания Web-узла. Создаваемый узел является активным, он может реагировать на действия пользователя, используя для этих целей технологию ASP(Active Server Page) страниц. Напомню, эти страницы содержат сценарии, которые собственно и определяют реакцию на действия пользователя. Особенностью ASP-страниц является то, что код сценариев выполняется на сервере и пользователю пересылается статическая страница, содержащая не код, а результат его выполнения. В рассматриваемом фрагменте рассматривается типичная для активных Web-узлов ситуация, когда пользователь заполняет некоторую форму и отсылает ее на сервер. ASP-страница обрабатывает данные формы, сохраняет их в базе данных, добавляя новые записи в таблицы, и формирует новую страницу, пересылаемую пользователю. Вот как выглядит страница узла, на которой пользователь заполняет форму:
увеличить изображение Рис. 6.13. Web-страница, содержащая форму, отсылаемую на сервер Приведу теперь текст ASP-страницы, которая выполняется на сервере:
<% 'Переменные, задающие поля формы Dim DAvtor, DFam, DName, DVid, DGorod, Dmail, DOtziv, DVopros, DPriob 'Передача значений полей формы DFam= Request.Form("Fam") DName = Request.Form("Name") DVid= Request.Form("Vid") DGorod =Request.Form("Gorod") Dmail= Request.Form("mail") DOtziv= Request.Form("Otziv") DPriob= Request.Form("Priob") DVopros =Request.Form("Vopros") DAvtor = Request.Form("Avtor")
'Объявление объектов ADO - Connection, Command, Recordset Dim Cconnect, Cmd, Records Dim StrSQL 'строка запроса
' Установить связь с базой данных Set Cconnect =Server.CreateObject("ADODB.Connection") Cconnect.ConnectionString= "provider=microsoft.jet.oledb.4.0;" & "data source ='d:\Bivant\svyaz.mdb'" Cconnect.Open
'Формирование строки запроса StrSQL = "Select * From VoprosOtvet"
'Создание команды и задание свойств объекта Command Set Cmd =Server.CreateObject("ADODB.Command") Cmd.ActiveConnection = Cconnect Cmd.CommandText = StrSQL Cmd.CommandType =1
'Работа с RecordSet Set Records =Server.CreateObject("ADODB.Recordset") With Records 'Открытие обновляемого объекта RecordSet .Open Cmd,,2,3
' Добавление записи .MoveFirst .AddNew .Fields("Автор")=DAvtor .Fields("Вопрос") = DVopros .Fields("Отзыв") = DOtziv .Fields("Фамилия") =DFam .Fields("Имя") =DName .Fields("Город") = DGorod .Fields("Вид_деят") =DVid .Fields("Почта") =Dmail .Fields("Приобрести") =DPriob .Update End With
%> Спасибо за Ваш интерес к нашей тематике!
Мы постараемся учесть Ваши пожелания и своевременно ответить на вопрос.
Приведу теперь комментарии к этому тексту:
Текст написан на HTML и представляет содержание ASP-страницы, в задачу которой входит обработать посылаемую пользователем форму. Большая часть текста задает сценарий, написанный на VBScript, и именно эту часть я и буду комментировать. Для удобства восприятия код сценария подсвечен.
Язык VBScript является нетипизированным языком, все переменные принадлежат к одному универсальному типу Variant, поэтому для переменных, соответствующих полям формы не задан тип.
Объект ASP Request позволяет передать значения из полей формы в переменные VBScript.
Затем начинается работа с объектами ADO, которые создаются на сервере методом CreateObject объекта Server.
Вся остальная часть работы с объектами ADO - создание соединения с базой данных, формирование команды, набора записей, добавление новой записи к объекту Recordset и обновление базы данных, выполняется на VBScript совершенно аналогично тому, как это делалось на VBA. В результате выполнения этого сценария база данных действительно изменяется и данные, посланные в форме, будут сохранены в соответствующей таблице базы данных.
На этом я закончу разговор о компонентах ADO, представляющих универсальный способ взаимодействия с самыми разными источниками данных.
<% 'Переменные, задающие поля формы Dim DAvtor, DFam, DName, DVid, DGorod, Dmail, DOtziv, DVopros, DPriob 'Передача значений полей формы DFam= Request.Form("Fam") DName = Request.Form("Name") DVid= Request.Form("Vid") DGorod =Request.Form("Gorod") Dmail= Request.Form("mail") DOtziv= Request.Form("Otziv") DPriob= Request.Form("Priob") DVopros =Request.Form("Vopros") DAvtor = Request.Form("Avtor")
'Объявление объектов ADO - Connection, Command, Recordset Dim Cconnect, Cmd, Records Dim StrSQL 'строка запроса
' Установить связь с базой данных Set Cconnect =Server.CreateObject("ADODB.Connection") Cconnect.ConnectionString= "provider=microsoft.jet.oledb.4.0;" & "data source ='d:\Bivant\svyaz.mdb'" Cconnect.Open
'Формирование строки запроса StrSQL = "Select * From VoprosOtvet"
'Создание команды и задание свойств объекта Command Set Cmd =Server.CreateObject("ADODB.Command") Cmd.ActiveConnection = Cconnect Cmd.CommandText = StrSQL Cmd.CommandType =1
'Работа с RecordSet Set Records =Server.CreateObject("ADODB.Recordset") With Records 'Открытие обновляемого объекта RecordSet .Open Cmd,,2,3
' Добавление записи .MoveFirst .AddNew .Fields("Автор")=DAvtor .Fields("Вопрос") = DVopros .Fields("Отзыв") = DOtziv .Fields("Фамилия") =DFam .Fields("Имя") =DName .Fields("Город") = DGorod .Fields("Вид_деят") =DVid .Fields("Почта") =Dmail .Fields("Приобрести") =DPriob .Update End With
%> Спасибо за Ваш интерес к нашей тематике!
Мы постараемся учесть Ваши пожелания и своевременно ответить на вопрос.