МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РЕСПУБЛИКИ КАЗАХСТАН ГОСУДАРСТВЕННЫЙ УНИВЕРСИТЕТ имени ШАКАРИМА г. СЕМЕЙ Документ СМК 3 уровня УМКД УМКД 042-39. 1.ХХ/032013 УМКД Редакция №____от_____ Учебно-методические материалы по дисциплине «Программирование базы данных» УЧЕБНО-МЕТОДИЧЕСКИИ КОМПЛЕКС ДИСЦИПЛИНЫ «Программирование базы данных» для специальности 5В011100 – «Информатика » УЧЕБНО-МЕТОДИЧЕСКИЕ МАТЕРИАЛЫ Семей 2014 СОДЕРЖАНИЕ 1 2 3 4 Глоссарий Лекции Практические и лабораторные занятия Самостоятельная работа студента 1. ГЛОССАРИИ Атрибут — элемент данных в кортеже. База данных — набор связанных данных. База Данных (БД) — структурированный организованный набор данных, описывающих характеристики каких-либо физических или виртуальных систем. База Данных (БД) — совместно используемый набор логически связанных дынных (и описание этих данных), предназначенный для удовлетворения информационных потребностей организации. Внешний ключ (foreign key) — поле таблицы, предназначенное для хранения значения первичного ключа другой таблицы с целью организации связи между этими таблицами. Домен — набор допустимых значений одного или нескольких атрибутов. Схема «звезды», схема звёздного соединения, звездоподобная схема, звёздная схема (от англ. star schema) — специальная организация реляционных таблиц, удобная для хранения многомерных показателей. Лежит в основе реляционного OLAP. Индекс — объект базы данных, создаваемый с целью повышения производительности выполнения запросов. Каталог данных (data directory) хранит информацию о месте и способе хранения данных. Концептуальное проектирование — сбор, анализ и редактирование требований к данным. Логическое проектирование — преобразование требований к данным в структуры данных. Модель данных - интегрированный набор понятий для описания данных, связей между ними и ограничений, накладываемых на данные в некоторой организации. Модель данных есть формальная теория представления и обработки данных в системе управления базами данных (СУБД), которая включает, по меньшей мере, три аспекта: 1. аспект структуры: методы описания типов и логических структур данных; 2. аспект манипуляции: методы манипулирования данными; 3. аспект целостности: методы описания и поддержки целостности базы данных. Нормальная форма — требование, предъявляемое к отношениям в теории реляционных баз данных для устранения из базы избыточности, которая потенциально может привести к логически ошибочным результатам выборки или изменения данных. Отношение — N-арным отношением R, или отношением R степени n, называют подмножество декартового произведения множеств D_1, D_2, ..., D_n (n\ge 1), не обязательно различных. Исходные множества D1,D2,...,Dn называют в модели доменами (в СУБД используется понятие тип данных). Отношение имеет простую графическую интерпретацию, оно может быть представлено в виде таблицы, столбцы (поля, атрибуты) которой соответствуют вхождениям доменов в отношение, а строки (записи) — наборам из n значений, взятых из исходных доменов. Число строк (кортежей) n, называют кардиальным числом отношения, или мощностью отношения. Первичный ключ (primary key) — минимальное множество атрибутов, являющееся подмножеством заголовка данного отношения, составное значение которых уникально определяет кортеж отношения. Поле — некая характеристика моделируемого объекта. Представления — виртуальная (логическая) таблица, результат запроса из базы данных. Реляционная алгебра — формальная система манипулирования отношениями в реляционной модели данных. Реляционная модель данных — логическая модель данных, строгая математическая теория, описывающая структурный аспект, аспект целостности и аспект обработки данных в реляционных базах данных. Система управления базами данных (СУБД) - программное обеспечение, управляющее доступом к БД. Системный каталог в реляционных СУБД представляет собой совокупность специальных таблиц, которыми владеет сама СУБД. Системный каталог (словарь данных) — совокупное описание данных, называемых метаданными (совокупность метаданных (данные о данных)). СУБД — специализированная программа (чаще комплекс программ), предназначенная для организации и ведения базы данных. СУБД - программное обеспечение, с помощью которого пользователи могут определять, создавать и поддерживать базу данных, а также осуществлять к ней контролируемый доступ. Схема — структура базы данных. Таблица — структура данных, хранящая набор однотипных записей. Транзакция — в информатике, группа последовательных операций, которая представляет собой логическую единицу работы с данными. Транзакция представляет собой набор действий, выполняемых отдельным пользователем или прикладной программой с целью доступа или изменения содержимого базы данных. Файл — простой набор записей, содержащих логически связанные данные. Файловая система (картотека) — набор программ, которые выполняют для пользователей некоторые операции, каждая программа определяет свои собственные данные и управляет ими. Физическое проектирование — определение особенностей хранения данных, методов доступа и т.д. Хеширование — преобразование входного массива данных произвольной длины в выходную битовую строку фиксированной длины. Целостность базы данных — соответствие имеющейся в базе данных информации её внутренней логике, структуре и всем явно заданным правилам. 2. ЛЕКЦИИ Лекция 1.Теория проектирования баз данных Цель: Ознакомление с терминологией, принятой в программировании БД, какие бывают связи между таблицами, что такое ссылочная целостность баз данных, а также ознакомление с проектированием и нормализацией таблиц и тремя нормальными формами. Программирование баз данных - очень большой и серьезный раздел самого что ни на есть практического программирования. На предыдущем курсе "Введение в программирование на Delphi " мы лишь коснулись этой темы, затронули даже не верхушку айсберга под названием Базы Данных, а только его макушку. Между тем, многие программисты большую часть своего времени тратят именно на проектирование баз данных и разработку приложений, работающих с ними. Это неудивительно - в настоящее время каждая государственная организация, каждая фирма или крупная корпорация имеют рабочие места с компьютерами. Имеется масса данных, которые нужно не только сохранить, но и обработать, получить комплексные отчеты. Без баз данных сегодня не обойтись. А завтра они будут еще нужней. Недостаточно просто написать программу, взаимодействующую с БД. Нужно уметь правильно спроектировать эту базу данных. Проектирование баз данных, в общем, является первым шагом разработки приложения. Только когда база данных спроектирована, программист приступает непосредственно к проекту приложения. На этой лекции мы коротко определимся с терминологией БД (для тех, кто пропустил курс "Введение в программирование на Delphi "), затем изучим вопросы проектирования баз данных. Этот курс лекций целиком и полностью посвящен базам данных и разработке приложений, обслуживающих их. На предыдущем курсе мы упоминали, что существуют такие типы баз данных: локальные, файл-серверные, клиент-серверные и распределенные БД. Нам с вами доводилось работать с локальными БД, однако многое осталось "за кадром" - в рамках одного курса просто невозможно дать материал по разнообразным темам, для каждой из которых написано немало книг. Здесь мы продолжим знакомство с локальными БД. Мы познакомимся с различными механизмами доступа к базам данных. Подробно изучим архитектуру клиент-сервер, которая является наиболее востребованной сегодня архитектурой программирования БД. Также рассмотрим механизмы создания распределенных (или многоуровневых) баз данных. Файл-серверные БД имеют очень ограниченные возможности, и в настоящий момент практически не используются, поэтому мы не будем касаться этой темы. Вместо этого гораздо удобней использовать распределенную архитектуру совместно с применением локальных технологий. Обо всем этом и о многом другом мы поговорим на этом курсе. На курсе "Введение в программирование на Delphi " мы пользовались BDE встроенным механизмом доступа к базам данных. Больше к этим темам мы возвращаться не будем, поэтому если вы пропустили этот курс, то хотя бы бегло просмотрите работу с BDE в лекциях 29-32. Тем не менее, в рамках изучения новых возможностей при работе с базами данных, мы кратко коснемся и BDE, наряду с другими технологиями доступа. Терминология Базой данных (БД) называется электронное хранилище информации, доступ к которому имеет один или несколько компьютеров. В былые времена под базой данных понимали файл, где данные хранились в табличном виде. Сейчас под базой данных обычно подразумевают папку, в которой хранится один или несколько файлов с таблицами. Эти таблицы, вместе или по отдельности, взаимодействуют с пользовательским приложением. Существуют базы данных, в которых таблицы, индексы и другие служебные данные хранятся в одном файле. К таким БД можно отнести, например, MSAccess и InterBase. В этом случае базой данных будет созданный файл. Таблицы имеющие связи между собой, называют реляционными, и базы данных, в которых имеются взаимосвязанные таблицы, также называются реляционными. Реляционные базы данных в настоящее время наиболее распространены. Часто пользовательские приложения не работают с базами данных напрямую. Имеются специальные программы, называемые Системы Управления Базами Данных ( СУБД ), которые служат посредниками между базой данных и пользовательским приложением. Такой подход называют архитектурой клиент-сервер, а такие СУБД часто называют серверами баз данных. Иногда еще добавляют букву Р ( РСУБД - Реляционная СУБД ). Однако не все СУБД предназначены для архитектуры клиент-сервер. Например, программа Access из пакета MSOffice - это СУБД, предназначенная для локального или файл-серверного использования. Основой любой БД является таблица. Таблица - это файл определенного формата с данными, представленными в табличном виде, например: Рис. 1.1 . Представление данных в табличном виде. Такая таблица состоит из полей и записей. Поле - столбец таблицы, имеющий название, тип данных и размер. Поле предназначено для описания отдельного атрибута записи. Например, поле "№" имеет целочисленный тип данных, а поле "Фамилия" - строковый. Запись - строка таблицы, описывающая какой-то объект, или иначе, набор атрибутов какого-то объекта. Например, строка под номером 1 описывает человека Иванова Ивана Ивановича. Первичный ключ - это поле или набор полей, однозначно идентифицирующих запись. В ключевом поле не может быть двух записей с одинаковым значением. Например, поле "Фамилия" нельзя делать ключевым, ведь в таблице могут оказаться однофамильцы. Поле "№" больше подходит для того, чтобы сделать его ключевым. Первичные ключи помогают упорядочить записи и облегчают установку связей между таблицами. Каждая таблица может содержать только один первичный ключ. Индекс - это поле или набор полей, которые часто используются для сортировки или поиска данных. Индексные поля еще называют вторичными ключами. В отличие от первичных ключей, поля для индексов могут содержать как уникальные, так и повторяющие значения. Например, поле "Фамилия" можно сделать индексным - ведь поиск и сортировка записей часто может производиться по этому полю. Индексы могут быть уникальными, то есть, не допускающими совпадений в записях, как первичные ключи, и не уникальными, допускающими такие совпадения. Индексы могут быть как в восходящем порядке (А, Б, …, Я), так и в нисходящем (Я, Ю, …, А). Таблица может иметь множество индексов. Можно все поля сделать индексными, причем даже на каждое поле по два индекса - в восходящем и нисходящем порядке. Однако при этом следует иметь в виду, что база данных в этом случае будет непомерно раздута, и работа с ней значительно замедлится. Другими словами, нужно соблюдать меру, и делать индексными только те поля, по которым действительно часто придется сортировать или фильтровать данные. Связи (отношения) Реляционные связи (отношения) между таблицами предназначены для разбивки таблиц на самодостаточные части. Рассмотрим пример. Допустим, люди из предыдущей таблицы - студенты. Таблица предназначена для того, чтобы указать, какие экзамены были сданы конкретным студентом. Следовательно, в таблицу требуется добавить поле "Экзамен": Рис. 1.2 . Исправленная таблица Сразу бросается в глаза недостаток такого проектирования: данные из полей "Фамилия", "Имя" и "Отчество" многократно повторяются. Пользователю придется вводить большое количество дублирующих данных, а таблица получается "распухшей", переполненной этими данными. Исправить положение несложно, нужно лишь разбить эту таблицу на две разных таблицы, имеющие релятивную связь: Рис. 1.3. Реляционная связь между таблицами Как вы можете заметить, избыточность данных исчезла - в одной таблице представлены только данные по студентам, в другой - данные по экзаменам. Связь между таблицами организована по ключевому полю "№" таблицы со студентами. В таблице с экзаменами, вместо полных данных о студенте вписывается только его номер. Студент может сдать сколько угодно много экзаменов, пользователь же просто выберет его из списка, и в таблицу попадет его номер. Такую таблицу легче заполнять, и размер ее будет тоже меньше. При создании связей, как правило, одна таблица называется главной (master), другая - подчиненной (details). В нашем случае главной является таблица со студентами. Таблица со списком сданных экзаменов - подчиненная. Связь, представленная в рисунке 1.3 называетсяотношением один-ко-многим. То есть, одна запись из одной таблицы может иметь связь с множеством записей из другой таблицы. Однако имеется возможность и того, что запись из первой таблицы не будет иметь никаких связей с другой таблицей - студент может еще не сдать ни одного экзамена. Отношение один-ко-многим встречается наиболее часто. Отношение один-к-одному подразумевает, что одной записи в главной таблице соответствует одна запись в подчиненной таблице. Взгляните на рисунок: Рис. 1.4. Отношение один-к-одному Данные о студентах, такие как фамилия, группа, могут часто использоваться для самых разных отчетов. Однако домашний адрес и телефон студентов нужны далеко не всегда, поэтому они вынесены в другую таблицу. Если бы мы объединили эти таблицы в одну, то получили бы таблицу с переизбытком данных. Связь один-к-одному используют для того, чтобы отделить главную информацию от второстепенных данных. Отношение многие-ко-многим встречается реже. Такое отношение подразумевает, что одна запись из главной таблицы может иметь связь со многими записями из подчиненной таблицы. А одна запись из подчиненной таблицы может иметь связь со многими записями главной таблицы. Рассмотрим следующий рисунок: Рис. 1.5 . Отношение многие-ко-многим Как видно из рисунка, один покупатель может купить несколько товаров, в то же время как один товар может быть куплен несколькими покупателями. Считается, что базу данных можно спроектировать так, чтобы любая связь многие-ко-многим была бы заменена одной или более связями один-ко-многим. В самом деле, подобные отношения сложно отлаживать. Не все СУБД поддерживают индексацию и контроль над ссылочной целостностью в таких связях, поэтому старайтесь избегать отношений многие-ко-многим. Ссылочная целостность Ссылочной целостностью называют особый механизм, осуществляемый средствами СУБД или программистом, ответственный за поддержание непротиворечивых данных в связанных релятивными отношениями таблицах. Ссылочная целостность подразумевает, что в таблицах, имеющих релятивные связи, нет ссылок на несуществующие записи. Взгляните на рис. 1.3. Если мы удалим из списка студента Иванова И.И., и при этом не изменим таблицу со сданными экзаменами, ссылочная целостность будет нарушена, в таблице с экзаменами появится "мусор" - данные, на которые не ссылается ни одна запись из таблицы студентов. Ссылочная целостность будет нарушена. Таким образом, если мы удаляем из списка студента Иванова И.И., следует позаботиться о том, чтобы из таблицы со сданными экзаменами также были удалены все записи, на которые ранее ссылалась удаленная запись главной таблицы. Существует несколько видов изменений данных, которые могут привести к нарушению ссылочной целостности: 1. Удаляется запись в родительской таблице, но не удаляются соответствующие связанные записи в дочерней таблице. 2. Изменяется запись в родительской таблице, но не изменяются соответствующие ключи в дочерней таблице. 3. Изменяется ключ в дочерней таблице, но не изменяется значение связанного поля родительской таблицы. Многие СУБД блокируют действия пользователя, которые могут привести к нарушению связей. Нарушение хотя бы одной такой связи делает информацию в БД недостоверной. Если мы, например, удалили Иванова И.И., то теперь номер 1 принадлежит Петрову П.П.. Имеющиеся связи указывают, что он сдал экзамены по математике и физике, но не сдавал экзаменов по русскому языку и литературе. Достоверность данных нарушена. Конечно, в таких случаях в качестве ключа обычно используют счетчик - поле автоинкрементного типа. Если удалить запись со значением 1, то другие записи не изменят своего значения, значение 1 просто невозможно будет присвоить какой-то другой записи, оно будет отсутствовать в таблице. Путаницы в связях не случится, однако все равно подчиненная таблица будет иметь "потерянные" записи, не связанные ни с какой записью главной таблицы. Механизм ссылочной целостности должен запрещать удаление записи в главной таблице до того, как будут удалены все связанные с ней записи в дочерней таблице. Нормализация базы данных Каждый программист обычно по-своему проектирует базу данных для программы, над которой работает. У одних это получается лучше, у других - хуже. Качество спроектированной БД в немалой степени зависит от опыта и интуиции программиста, однако существуют некоторые правила, помогающие улучшить проектируемую БД. Такие правила носят рекомендательный характер, и называются нормализацией базы данных. Процесс нормализации данных заключается в устранении избыточности данных в таблицах. Существует несколько нормальных форм, но для практических целей интерес представляют только первые три нормальные формы. Первая нормальная форма ( 1НФ ) требует, чтобы каждое поле таблицы БД было неделимым (атомарным) и не содержало повторяющихся групп. Неделимость означает, что в таблице не должно быть полей, которые можно разбить на более мелкие поля. Например, если в одном поле мы объединим фамилию студента и группу, в которой он учится, требование неделимости соблюдаться не будет. Первая нормальная форма требует, чтобы мы разбили эти данные по двум полям. Под понятием повторяющиеся группы подразумевают одинаковые по смыслу значения. Взгляните на рисунок: поля, содержащие Рис. 1.6 . Повторяющиеся группы Верно, такую таблицу можно сделать, однако она нарушает правило первой нормальной формы. Поля "Студент 1", "Студент 2" и "Студент 3" содержат одинаковые по смыслу объекты, их требуется поместить в одно поле "Студент", как в рисунке 1.4. Ведь в группе не бывает по три студента, правда? Представляете, как будет выглядеть таблица, содержащая данные на тридцать студентов? Это тридцать одинаковых полей! В приведенном выше рисунке поля описывают студентов в формате "Фамилия И.О.". Однако если оператор будет вводить эти описания в формате "Фамилия Имя Отчество", то нарушается также правило неделимости. В этом случае каждое такое поле следует разбить на три отдельных поля, так как поиск может вестись не только по фамилии, но и по имени или по отчеству. Вторая нормальная форма (2НФ) требует, чтобы таблица удовлетворяла всем требованиям первой нормальной формы, и чтобы любое не ключевое поле однозначно идентифицировалось полным набором ключевых полей. Рассмотрим пример: некоторые студенты посещают спортивные платные секции, и ВУЗ взял на себя оплату этих секций. Взгляните на рисунок: Рис. 1.7 . Нарушение второй нормальной формы В чем здесь нарушение? Ключом этой таблицы служат поля "№ студента" "Секция". Однако данная таблица также содержит отношение "Секция" - "Плата". Если мы удалим запись студента № 110, то потеряем данные о стоимости секции по скейтборду. А после этого мы не сможем ввести информацию об этой секции, пока в нее не запишется хотя бы один студент. Говорят, что такое отношение подвержено как аномалии удаления, так и аномалии вставки. В соответствие с требованиями второй нормальной формы, каждое не ключевое поле должно однозначно зависеть от ключа. Поле "Плата" в приведенном примере содержит сведения от стоимости данной секции, и ни коим образом не зависит от ключа номера студента. Таким образом, чтобы удовлетворить требованию второй нормальной формы, данную таблицу следует разбить на две таблицы, каждая из которых зависит от своего ключа: Рис. 1.8 . Правильная вторая нормальная форма Мы получили две таблицы, в каждой из которых не ключевые данные однозначно зависят от своего ключа. Третья нормальная форма ( 3НФ ) требует, чтобы в таблице не имелось транзитивных зависимостей между не ключевыми полями, то есть, чтобы значение любого поля, не входящего в первичный ключ, не зависело от другого поля, также не входящего в первичный ключ. Допустим, в нашей студенческой базе данных есть таблица с расходами на спортивные секции: Рис. 1.9 . Нарушение третьей нормальной формы Как нетрудно заметить, ключевым полем здесь является поле "Секция". Поля "Плата" и "Кол-во студентов" зависят от ключевого поля и не зависят друг от друга. Однако поле "Общая стоимость" зависит от полей "Плата" и "Кол-во студентов", которые не являются ключевыми, следовательно, нарушается правило третьей нормальной формы. Поле "Общая стоимость" в данном примере можно спокойно убрать из таблицы, ведь если потребуется вывести такие данные, нетрудно будет перемножить значения полей "Плата" и "Кол-во студентов", и создать для вывода вычисляемое поле. Таким образом, нормализация данных подразумевает, что вы вначале проектируете свою базу данных: планируете, какие таблицы у вас будут, какие в них будут поля, какого типа и размера. Затем вы приводите каждую таблицу к первой нормальной форме. После этого приводите полученные таблицы ко второй, затем к третьей нормальной форме, после чего можете утверждать, что ваша база данных нормализована. Однако такой подход имеет и недостатки: если вам требуется разработать программный комплекс для крупного предприятия, база данных будет довольно большой. При нормализации данных, вы можете получить сотни взаимосвязанных между собой таблиц. С увеличением числа нормализованных таблиц уменьшается восприятие программистом базы данных в целом, то есть вы можете потерять общее представление вашей базы данных, запутаетесь в связях. Кроме того, поиск в чересчур нормализованных данных может быть замедлен. Отсюда вывод: при работе с данными большого объема ищите компромисс между требованиями нормализации и собственным общим восприятием базы данных. Лекция 2.ADO. Связь с таблицей MSAccess Цель:Рассмотрение механизмов доступа к данным BDE и ADO, обсуждение их плюсы и минусы. Создание базу данных с помощью программы MSAccess, создание приложение, которое работает с этой базой данных, используя механизм ADO. С самого появления технологии баз данных программисты испытывали потребность в механизмах доступа к этим самым данным. Различные компании по-своему пытались предоставить им такую возможность. Например, для работы с таблицами типа dBase была создана Система Управления Базами Данных ( СУБД ) Clipper. Для времен операционной системы MS-DOS - превосходное решение. Однако Clipper не мог работать ни с какими другими типами таблиц. И чем больше типов данных появлялось, тем острее вставала необходимость разработать универсальный инструмент доступа, который мог бы работать с любым типом данных. Механизм доступа к данным - это программный инструмент, позволяющий получить доступ к базе данных и ее таблицам. Как правило, это драйвер в виде *.dll файлов, который устанавливается на ПК разработчика (и клиента), и который используется программой для связи с БД. Сравнение BDE и ADO BorlandDatabaseEngine (BDE) - первая такая разработка фирмы Borland. Этот механизм доступа к данным позволяет обращаться к локальным и файл-серверным форматам баз данных dBase, FoxPro и Paradox, к различным серверам SQL и ко многим другим источникам данных, доступ которых поддерживался при помощи драйверов ODBC. Например, с помощью BDE можно напрямую работать с табличными файлами MSExcel. Увы, механизм доступа BDE признается устаревшим даже самой компанией Borland. В данный момент многие инструменты Delphi являются кросс платформенными, то есть, программы с небольшими доработками можно переносить на другие операционные системы. Корпорация Borland выпустила новую среду быстрой разработки программ - Kylix, на которой создаются приложения для операционных систем семейства Linux. Часто говорят, что Kylix - это Delphi для Linux. Так и есть - если вы умеете программировать на Delphi, сумеете и на Kylix. Большинство инструментов Delphi были унаследованы Kylix, но, увы, не BDE. Дальнейшее развитие этого механизма доступа к данным корпорацией Borland прекращено. Тем не менее, хоронить его рано. Многие программисты до сих пор используют данный инструмент в разработке приложений для небольших компаний. Да что там говорить, китайская компания Huawei, разрабатывающая современнейшие электронные АТС как для городских, так и для мобильных телефонов, до сих пор использует BDE для доступа к настройкам и статистическим данным этих АТС! Кроме того, BDE имеет множество простых и удобных возможностей для программиста, таких например, как создание таблиц программно. Удобство работы с BDE трудно переоценить, однако нельзя не сказать и о минусах. Основной минус - распространение приложений. Если ваше приложение использует для доступа к данным компоненты BDE, то и у клиента, который будет пользоваться вашей программой, должен быть установлен BDE. Причем если вы использовали алиасы (псевдонимы базы данных), то настройка на эти же алиасы должна быть и у клиента. Впрочем, создание инсталляционного пакета при помощи стандартной утилиты InstallShieldExpress снимает эту проблему. Эта утилита позволяет включать настроенный механизм BDE в состав инсталляционного пакета вашей программы. Конечно, за это приходится расплачиваться большими размерами инсталляционного файла. Другой минус касается не только BDE, но и любого другого универсального механизма доступа к данным. Универсальность такого механизма подразумевает сложность его реализации. Программисту предоставляется уже готовый инструмент, с которым удобно работать, однако этот инструмент достаточно "тяжелый" - используя его, вы довольно существенно увеличиваете размеры своего приложения. На предыдущем курсе "Введение в программирование на Delphi " мы затрагивали работу с базами данных посредством BDE. Больше к этим темам мы возвращаться не будем, хотя изредка будем обращаться к BDE, чтобы продемонстрировать те или иные возможности, отсутствующие в других механизмах доступа к данным, или отличающиеся от них. Поэтому если вы пропустили этот курс, то хотя бы бегло просмотрите работу с BDE в лекциях 29-32. ActiveXDataObject (ADO) - это механизм доступа к данным, разработанный корпорацией Microsoft. Если точнее, то ADO - это надстройка над технологией OLEDB, посредством которой можно связываться с различными данными приложений Microsoft. В середине 1990-х годов большое развитие получила технология COM, и корпорация Microsoft в связи с этим объявила о постепенном переходе от старой технологии ODBCк новой OLEDB. Однако технология OLEDBдостаточно сложная, использование этой технологии происходит на системном уровне и требует от программиста немало знаний и труда. Кроме того, технология OLEDB очень чувствительна к ошибкам, и "вылетает" при первом удобном случае. Чтобы облегчить программистам жизнь, корпорация Microsoft разработала дополнительный прикладной уровень ADO, который мы будем изучать на этом курсе. По своим возможностям ADO напоминает BDE, хотя конечно, является более мощным инструментом. Компания Borland разработала набор компонентов для доступа к ADO и первоначально назвала его ADOExpress. Однако корпорация Microsoft упорно противится использованию своих обозначений в продуктах сторонних разработчиков, поэтому, начиная с Delphi 6, этот набор компонентов стал именоваться dbGo. Эти компоненты вы можете увидеть на вкладке ADO палитры компонентов. Технология ADO, как и BDE, независима от конкретного сервера БД, имеет поддержку как локальных баз данных различных типов, так и некоторых клиентсерверных БД. Плюсов у этой технологии много. Драйверы, разработанные корпорацией Microsoft для собственных нужд, более надежные, чем драйверы сторонних производителей. Поэтому если вам требуется работать с базами данных MSAccess или для архитектуры клиент-сервер использовать MSSQLServer, то использование ADO будет наиболее предпочтительным. Кроме того, имеется плюс и в вопросе распространения программ - во всех современных Windows встроены драйверы ADO. Другими словами, ваша программа будет работать на любом ПК, где установлен Windows. Как ни странно, но основной минус так же заключается в вопросе распространения программ. Корпорация Microsoft поступает довольно хитро. Каждые пару-тройку лет появляются новые версии Windows. Рядовому пользователю обычно нет нужды переходить на свежую ОС, тем более что каждая новая система становится все требовательней к ресурсам ПК. Для того чтобы заставить пользователя перейти на новую версию, корпорация Microsoft обязательно вводит несколько новых стандартов или технологий, несовместимых со старыми. А для старых версий доработок не предусматривается. Вот и приходится бедному пользователю скрепя зубы тратиться на новые версии операционной системы и пакета MSOffice. Поэтому при использовании технологии ADO приходится думать о том, какая версия Windows стоит у конечного пользователя, будет ли ваша программа работать у него на ПК. Технология ADO на самом деле является частью Microsoft Data Access Components (MDAC). Компания Microsoft распространяет MDAC как отдельный продукт, к счастью, бесплатный. При этом поддерживается только самая последняя версия MDAC. Например, в состав Delphi 7 входит MDAC 2.6. При распространении собственных программ следует учитывать, что у клиента с большей долей вероятности уже установлена эта самая MDAC, причем самой последней версии. Однако если он пользуется старыми версиями Windows (Win95, 98, ME, NT), то вам потребуется позаботиться об установке MDAC на его компьютер. Если же у него установлена ОС Win2000, WinXP или более новая, то MDAC у него уже есть, и вам беспокоиться не о чем. Еще один серьезный минус ADO в том, что он для подключения к БД использует довольно медлительную технологию COM. Если ваша база данных будет содержать несколько тысяч записей, то скорость работы с таблицами может стать в сотни раз более медленной, чем если бы вы использовали BDE! На современных ПК, имеющих частоту процессора до 2 ГГц и выше, эти замедления могут быть и незаметны, но работа с огромной базой данных на более медленных ПК превратится в сплошное ожидание. Основными компонентами, с которыми нам предстоит работать, являются TADOConnection (для подключения к БД), TADOTable (аналог TTable из BDE), TADOQuery (аналог TQuery из BDE, для выполнения запросов и получения набора данных) и TADODataSet (предназначенный для набора данных, полученных через SQLзапрос). Создание базы данных MSAccess Базы данных MSAccess имеют много плюсов, часто программисты предпочитают использовать именно их. Во-первых, база данных MSAccess - это один файл. Сколько бы таблиц и индексов она не содержала, все это хранится в одном единственном файле. А значит, такую базу данных легче обслуживать - переносить на новое место, делать резервные копии и так далее. Еще один плюс - имена полей в такой БД можно давать русскими буквами. Лучше всего изучать новый материал на практике. Для примера создадим базу данных для отдела кадров какого-нибудь предприятия. Какие данные на сотрудника нам понадобятся? Прежде всего, фамилия, имя и отчество. Затем укажем пол (мужской или женский), семейное положение (холост или женат/замужем), количество детей. Также понадобятся дата рождения и дата поступления на работу. Стаж работы в годах. Образование. Военнообязанный сотрудник, или нет. Телефоны, по которым можно связаться с сотрудником в любое время. Должность сотрудника и отдел (если есть), в котором он числится. А также его домашний адрес. При этом учитываем, что сотрудник не обязательно является жителем города, где он работает. А вдруг он приехал на заработки из другого города? Или даже из другой страны? Следовательно, придется вводить и страну, и город - вдруг потребуется делать отчет по сотрудникам, прописанным в Украине, например? Вот сколько данных нужно будет вводить для отдела кадров! А ведь мы еще немного упростили их. Стаж работы подразделяется на общий и непрерывный. Эти данные учитываются при расчете больничных листов. Но для учебной базы данных такими деталями можно пренебречь. Итак, первым делом оптимизируем данные, исходя из правил трех нормальных форм. В результате получаем целых четыре таблицы: Рис. 2.1. Оптимизированные таблицы Главной здесь будет таблица LichData, которая содержит основные данные о сотруднике. Она имеет релятивные связи с другими таблицами. Поле "Ключ" будет автоинкрементным, то есть автоматически будет прибавляться на единицу, гарантируя нам уникальность ключа. В подчиненных таблицах имеется поле "Сотрудник" целого типа, по которому будет обеспечиваться связь. Причем ключевых полей в дочерних таблицах не будет. Главная таблица поддерживает связь один-к-одному с таблицами Doljnost и Adres, и связь один-ко-многим с таблицей Telephones, ведь у сотрудника наверняка есть и домашний, и рабочий телефоны, а в карманах, возможно, лежит пару мобильников. То есть, один сотрудник может иметь много телефонов. С полями и связями определились, пора создавать базу данных. Для этого загрузите программу MSAccess. Если в правой части окна у вас нет панели "Создание файла", то выберите команду "Файл -Создать". Затем выберите команду "Новая база данных". Сразу же выйдет запрос с именем этой базы данных. Создайте папку, которая все равно нам понадобится для нового проекта, укажите эту папку, а базу данных назовите ok (отдел кадров). Как только вы нажмете кнопку "Создать", появится окно этой базы данных: Рис. 2.2. Создание БД Сейчас нам потребуется сделать четыре таблицы. Поэтому дважды щелкаем по команде "Создание таблицы в режиме конструктора", и переходим к конструктору. В левой части мы вводим имя поля, причем русскими буквами. В поле "Тип данных выбираем тип", а на вкладке "Общие" делаем настройки поля. Описание поля заполнять необязательно. Итак, создаем поля: 1. "Ключ". Разумеется, имя поля пишем без кавычек. Выбираем тип данных "Счетчик", это автоинкрементный тип данных. В настройках убедитесь, что поле индексированно - Да (Совпадения не допускаются). Правой кнопкой щелкните по этому полю и выберите команду "Ключевое поле". Слева от поля появится значок ключа. 2. "Фамилия". Тип поля текстовый, размер 25 символов. Индексированное поле - Да (Совпадения допускаются). Ведь могут же попасться родственники или однофамильцы! 3. "Имя". Тип поля текстовый, размер 25 символов. Индексированное поле - Да (Совпадения допускаются). 4. "Отчество". Тип поля текстовый, размер 25 символов. Индексы не нужны. 5. "Пол". Текстовый, размер 3 символа. В формате поля укажите "муж/жен", конечно, без кавычек. 6. "Сем_Полож". Логический тип, формат поля "Да/Нет". Здесь мы будем указывать, состоит ли сотрудник (сотрудница) в браке. 7. "Детей". Числовой тип, размер поля Байт (трудно представить, что у кого-то будет более 255 детей!). 8. "Дата_Рожд". Тип поля - Дата/Время. Выберите формат "Краткая форма даты". Затем выберите тот же формат для поля "Маска". При попытке выбора маски выйдет запрос на подтверждение сохранения таблицы. Ответьте утвердительно, а вместо имени таблицы по умолчанию "Таблица 1" впишите " LichData ", так будет называться наша первая таблица. После этого появится окно создания маски ввода. Выберите "Краткий формат даты", нажмите "Далее", после чего в окне "Маска ввода" наберите "00.00.0000". В результате мы будем иметь маску в виде "дд.мм.гггг". 9. "Дата_Пост". Все то же самое, что и в №8. 10. "Стаж". Тип поля числовой, размер - байт. 11. "Образование". Текстовый, размер поля 30 символов. Ведь здесь может быть и длинный текст, например "неоконченное высшее техническое". 12. "Военнообязанный". Логический тип, формат "Да/Нет". В результате получим такую картину: Рис. 2.3. Поля таблицы LichData При попытке закрыть это окно, выйдет запрос о сохранении таблицы " LichData ". Ответьте утвердительно. Главная таблица сделана, осталось еще три. Снова щелкаем "Создание таблицы в режиме конструктора". Вводим такие поля: 1. "Сотрудник". Тип поля - числовой, размер поля - длинное целое. Делать это поле ключевым не нужно, даже после того, как при попытке закрыть таблицу Access предложит вам сделать поле ключевым. 2. "Отдел", Текстовое, 15 символов. 3. "Должность", Текстовое, 20 символов. Закрываем таблицу, даем ей имя " Doljnost ", отказываемся от создания ключа. Делаем следующую таблицу. Поля: 1. "Сотрудник". Тип поля - числовой, размер поля - длинное целое. Не ключевое. 2. "Страна". Тип текстовый, размер 15. 3. "Город". Тип текстовый, размер 20. 4. "Дом_Адрес". Тип текстовый, размер 100. Закрываем таблицу, даем имя " Adres ", отказываемся от создания ключа. Делаем следующую таблицу. Поля: 1. "Сотрудник". Тип поля - числовой, размер поля - длинное целое. Не ключевое. 2. "Телефон". Тип текстовый, размер 17. Желательно задать маску. Сразу же выйдет запрос о сохранении таблицы, сохраните ее под именем " Telephones ". Для этого выбираем маску (дважды щелкаем по ней), в окне нажимаем кнопку "Список". Настраиваем маску, как на рисунке: Рис. 2.4.Маска для телефона 3. "Примечание". Тип текстовый, размер 10. Формат "Рабочий/Домашний/Мобильный". Закрываем таблицу " Telephones ", отказываясь от создания ключевого поля. Все, база данных готова. Программу MSAccess можно закрыть, больше она не понадобится. Пока база данных еще пустая, желательно сделать резервную копию файла ok.mdb, который и является полученной базой данных. Как видите, никаких связей между таблицами мы не делали - проще будет сделать их в проекте программы. Лекция 3. Поиск, фильтрация и индексация таблиц Цель: Ознакомление с различными методами поиска нужной записи в таблице, с применением фильтрации записей, удовлетворяющих нужному условию, с использованием индексных полей для сортировки данных в возрастающем и в убывающем порядке. Последовательный перебор В программах, работающих с базами данных, часто используют поиск данных. Для чего еще нужны базы данных, как не для этого? Самый простой, но в то же время и самый медленный, "тяжеловесный" поиск, это, пожалуй, последовательный перебор. Вы переходите на первую запись таблицы, создаете цикл, который длится до последней записи, и внутри этого цикла проверяете необходимое условие. Также можно делать и обратный перебор, от последней записи к первой. В таблице 3.1 приведены все свойства и методы наборов данных (TTable/ADOTable, TQuery/ADOQuery), которые могут быть использованы при организации последовательного перебора: Таблица 3.1. Свойства и методы набора данных, которые могут быть задействованы при последовательном переборе Свойства и Описание методы Eof Свойство логического типа. Принимает значение True, если достигнут конец таблицы, или если таблица пуста, и False в противном случае. Bof Свойство логического типа. Принимает значение True, если достигнуто начало таблицы, и False в противном случае. Next Метод. Делает текущей следующую запись набора данных. Prior Метод. Делает текущей предыдущую запись набора данных. First Метод. Делает текущей первую запись набора данных. Last Метод. Делает текущей последнюю запись набора данных. Пример: //перешли на первую запись: fDM.TLichData.First; //делать, пока не конец таблицы: while not fDM.TLichData.Eof do begin if fDM.TLichData['Фамилия'] = 'Иванов' then break; //нашли нужную запись, и вышли из цикла fDM.TLichData.Next; //иначе перешли на следующую запись end; //while Как видно из примера, мы делаем прямой последовательный перебор от первой записи до последней. Получить или изменить значение нужного поля можно, указав имя поля в квадратных скобках после имени набора данных. Например: Edit1.Text := fDM.TLichData['Фамилия']; //получили значение fDM.TLichData['Фамилия']:= Edit1.Text; //изменили значение Приведенный пример поиска нужной записи допустим, если в таблице имеется не более сотни-другой записей, а условная проверка достаточно сложна. Но обычно программисты этот способ не используют, или используют только в крайнем случае. Далее рассмотрим другие способы поиска. Метод Locate Метод Locate ищет первую запись, удовлетворяющую условию поиска. Если запись найдена, метод делает ее текущей и возвращает True. В противном случае метод возвращает False и курсор не меняет положения. Поле, по которому ведется поиск, не обязательно должно быть индексировано. Однако если поле индексировано, то метод ищет запись по индексу, что значительно ускоряет поиск. Поиск может вестись как по одному полю, так и по нескольким полям. Метод имеет три параметра: function Locate (const KeyFields: String; const KeyValues: Variant; Options: TLocateOptions) :Boolean; Параметр KeyFields задает поле или список полей, по которым ведется поиск. Если имеется несколько полей, их разделяют точкой с запятой. Параметр KeyValues является вариантным массивом, в котором задаются критерии поиска. При этом первое значение KeyValues ставится в соответствие с первым полем, указанным в KeyFields. Второе - со вторым, и так далее. Третий параметр Options позволяет задать некоторые опции поиска: loCaseInsensitive - поиск ведется без учета высоты букв, то есть, считаются одинаковыми строки "строка", "Строка" или "СТРОКА". loPartialKey - запись будет удовлетворять условию, если ее часть содержит искомый текст. То есть, если мы ищем "ст", то удовлетворять условию будут "строка", "станция", "стажер" и т.п. Пустой набор[]указывает, что настройки поиска игнорируются. То есть, строка ищется "как есть". Примеры использования метода Locate: Table1.Locate('Фамилия', Edit1.Text, []); Table1.Locate('Фамилия;Имя', VarArrayOf(['Иванов', 'Иван']), [loCaseInsensitive]); Как видно из примера, если для поиска вы используете одно поле, то значение может передаваться напрямую из компонента Edit. Если же вы используете список полей, то должны передать в метод массив вариантов, в которых содержатся искомые значения, по одному на каждое поле. При установке компонента ADOTable в раздел uses прописывается модуль ADODB, который содержит описания всех свойств, методов и событий компонента. Желательно использовать метод в том модуле, где установлен этот компонент. Рассмотрим применение этого метода на примере. Откройте проект. Перейдите на модуль DM, где у нас хранятся компоненты доступа к базе данных. Процедуру поиска реализуем в этом модуле, а чтобы с ней можно было работать из других форм, опишем ее в разделе public: public { Public declarations } procedure MyLocate(s: String); Как видите, в процедуру передается параметр - строка. В ней мы будем передавать искомую фамилию. Если курсор находится на описании нашей процедуры, то нажмите <Ctrl + Shift + C>, чтобы сгенерировать процедуру автоматически. Процедура будет иметь следующий код: procedureTfDM.MyLocate(s: String); begin TLichData.Locate('Фамилия', s, [loPartialKey]); end; Таким образом, при нахождении подходящей записи курсор будет перемещаться к ней. На главной форме выделите компонент Edit, предназначенный для поиска по фамилии. Создайте для него событие onChange, которое наступает при изменении текста в поле компонента. В созданной процедуре пропишите вызов поиска: fDM.MyLocate(Edit1.Text); Сохраните пример, скомпилируйте и опробуйте результаты поиска. Метод Locate рекомендуется использовать везде, где это возможно, поскольку он всегда пытается применить наиболее быстрый поиск. Если поле индексировано, и использование индекса ускорит процесс поиска, Locate использует индекс. Если поле не имеет индекса, Locate все равно ищет данные наиболее быстрым способом. Это делает вашу программу независимой от индексов. Метод Lookup Метод Lookup, в отличие от Locate, не меняет положение курсора в таблице. Вместо этого он возвращает значения некоторых ее полей. Причем в отличие от Locate, этот метод осуществляет поиск лишь на точное соответствие. Такой способ поиска востребован реже, однако в иных случаях этим методом очень удобно пользоваться. Рассмотрим синтаксис этого метода. function Lookup (const KeyFields: String; const KeyValues: Variant; constResultFields: String) : Variant; Как вы видите, первые два параметра такие же, как у Locate. А вот третий параметр и возвращаемое значение отличаются. В строке ResultFields через точку с запятой перечисляются поля таблицы, значения которых метод должен вернуть. Возвращаются эти значения в виде вариантного массива. Проблема в том, что вернуться может значение Null, то есть, ничего, или Empty (пустой) и это нужно проверять. Рассмотрим работу метода Lookup на примере нашей программы. Прежде всего, вспомним, как работает тип данных Variant. В переменную типа Variant можно поместить любое значение, в том числе и массив. Этот тип данных обычно используют, когда не известно заранее, данные какого типа нам понадобятся на этапе выполнения программы. Когда переменной типа Variant присвоено значение, имеется возможность проверить тип данных этого значения. Для этого служит функция VarType(): function VarType(const V: Variant): TVarType; В качестве параметра в функцию передается переменная вариантного типа. Функция возвращает значение типа TVarType. Это значение указывает, какого типа данные содержатся в переменной. Значение может быть varSmallint (короткое целое), varInteger (целое), varCurrency (денежный формат) и так далее. Чтобы увидеть полный список возвращаемых функцией значений, в редакторе кода установите курсор на название функции и нажмите <Ctrl + F1>, вызвав контекстный справочник. Нас же в данном примере интересуют всего два значения: varNull (записи нет) и varEmpty (запись пустая). Если в программе мы заранее не проведем проверку на эти значения, то вполне можем вызвать ошибку программы. Если же поиск прошел успешно, то будет возвращен массив вариантных значений, элементы которого начинаются с нуля. Каждый элемент массива будет содержать данные одного из указанных полей. Загрузите проект программы. Для поиска воспользуемся кнопкой с надписью "Найти", расположенной в верхней части главной формы. Идея такова: пользователь вводит в поле Edit1 какую то фамилию и нажимает кнопку "Найти". Событие onClick этой кнопки собирает в строковую переменную значения четырех указанных полей найденной записи. Причем после каждого значения в строку добавляется символ "#13" (переход на новую строку), формируя многострочный отчет. Затем эту строку мы выведем на экран функцией ShowMessage(). Итак, в окне главной формы дважды щелкните по кнопке "Найти", генерируя событие onClick. Полный листинг процедуры приведен ниже: {щелкнули по кнопке Найти} procedureTfMain.BitBtn1Click(Sender: TObject); var myLookup: Variant; //для получения результата s :String; //для отчета begin //получаем результат: myLookup := fDM.TLichData.Lookup('Фамилия', Edit1.Text, 'Фамилия;Имя;Отчество;Образование'); //проверяем, не Null ли это: ifVarType(myLookup) = varNullthen ShowMessage('Сотрудник с такой фамилией не найден!') else if VarType(myLookup) = varEmpty then ShowMessage('Запись не найдена!') //если это массив, то из его элементов собираем //многострочную строку: else if VarIsArray(myLookup) then begin s := myLookup[0] + #13 + myLookup[1] + #13 + myLookup[2] + #13 + myLookup[3]; //и выводим ее на экран: ShowMessage(s); end; //else if end; Комментарии достаточно подробны, чтобы вы разобрались с кодом. Сохраните проект, скомпилируйте его и запустите. Опробуйте этот способ поиска. Фильтрация данных Фильтрацию данных применяют не реже а, пожалуй, даже чаще, чем поиск. Разница в том, что при поиске данных пользователь видит все записи таблицы, при этом курсор либо переходит к искомой записи, либо он получает данные этой записи в виде результата работы функции. При фильтрации дело обстоит иначе. Пользователь в результате видит только те записи, которые удовлетворяют условиям фильтра, остальные записи становятся скрытыми. Конечно, таким образом искать нужные данные проще. Можно указать в условиях фильтра, что требуется вывести всех сотрудников, чья фамилия начинается на "И". Пользователь увидит только их. А можно и по-другому: вывести всех сотрудников, которые поступили на работу в период между 2000 и 2005 годом. Короче говоря, удобство работы пользователя с вашей программой зависит от вашей фантазии. Рассмотрим основные способы фильтрации записей. Свойство Filter Свойство Filter - наиболее часто используемый способ фильтрации записей, имеет тип String. Вначале программист задает условия фильтрации в этом свойстве, затем присваивает логическому свойству Filtered значение True, после чего таблица будет отфильтрована. Условия фильтрации должны входить в строку, например: fDM.TLichData.Filter := 'Фамилия =''Иванов'''; По правилам синтаксиса, если внутри строки встречается апостроф, его нужно дублировать. Приведенный выше пример в результате содержит условие: Фамилия = 'Иванов' Применяя это свойство, достаточно сложных условий задать невозможно, но если условия фильтрации просты, то данный способ незаменим. Опробуем фильтрацию записей на примере нашего приложения. Откройте событие onChange компонента Edit, изменим его немного. Закомментируйте или удалите вызов процедуры поиска MyLocate, и впишите следующий код: //fDM.MyLocate(Edit1.Text); - закомментировали fDM.TLichData.Filter := 'Фамилия >=' + QuotedStr(Edit1.Text); fDM.TLichData.Filtered := True; Откомпилируйте проект и запустите его на выполнение. При введении только первой буквы фамилии записи уже начинают фильтроваться. К примеру, если мы ввели букву "Л", то остаются записи с фамилиями, начинающимися от буквы "Л" до конца алфавита. Можно также улучшить поиск, если при этом еще отсортировать записи по индексу, но об этом чуть позже. Функция QuotedStr() возвращает переданный ей текст, заключенный в апострофы. Условие фильтра можно было бы описать и так: fDM.TLichData.Filter := 'Фамилия >=''' + Edit1.Text + ''''; Сложность заключается в том, что в этом случае приходится считать апострофы. Функция QuotedStr() помогает решить эту проблему. Событие onFilterRecord Это событие возникает при установке значения True в свойстве Filtered. Применение этого способа имеет большой плюс, и большой минус. Плюс в том что, сгенерировав это событие, программист получает возможность задать гораздо более сложные условия фильтрации. Минус же заключается в том, что проверка заключается перебором всех записей таблицы. Если таблица содержит очень много записей, процесс фильтрации может затянуться. В событие передаются два параметра. Первый параметр - набор данных DataSet. С ним можно обращаться, как с именем фильтруемой таблицы. Второй параметр логическая переменная Accept. Этой переменной нужно передавать результат условия фильтра. Если условие возвращает False, то запись не принимается, и не будет отображаться. Соответственно, если возвращается True, то запись принимается. Рассмотрим этот способ на примере. Суть примера в следующем: необходимо отфильтровать записи по начальным (или всем) буквам фамилии, вводимым пользователем в поле Edit1. В предыдущем примере, если бы мы ввели букву "И", то вышли бы фамилии, первой буквой которых были бы "И" - "Я". Это не так удобно. Сделаем так, чтобы если пользователь введет букву "И", то останутся только фамилии, начинающиеся на "И". Если пользователь введет еще букву "в", то останутся только фамилии, начинающиеся на "Ив", и так далее. Поочередно вводя начальные буквы, пользователь доберется до нужных фамилий. Для начала подготовим модуль данных. В нем нам потребуется создать глобальную переменную ed, чтобы мы могли передавать в нее текст из компонента Edit1: var fDM: TfDM; ed: String; //текст из Edit1 Этого действия можно было бы избежать, если бы компонент ADOTable, подключенный к таблице LichData, располагался на главной форме. Но поскольку он находится в модуле данных, то и событие onFilterRecord будет сгенерировано в нем. А в этом событии нам нужно будет знать, что в данный момент находится в поле ввода Edit1. Именно для этого и нужна глобальная переменная ed. Далее выделяем TLichData, то есть, компонент ADOTable, подключенный к таблице LichData. На вкладке Events (События) инспектора объектов найдите событие onFilterRecord и дважды щелкните по нему, сгенерировав процедуру. Полный листинг процедуры: {onFilterRecord главной таблицы} procedureTfDM.TLichDataFilterRecord(DataSet: TDataSet; var Accept: Boolean); var s : String; //для значения поля begin //получаем столько начальных букв из поля Фамилия, //сколько букв имеется в переменной ed: s := Copy(DataSet['Фамилия'], 1, Length(ed)); //делаем проверку на совпадение значений: Accept :=s = ed; end; Здесь в переменную s попадает столько начальных букв из поля "Фамилия", сколько букв содержит в данный момент компонент Edit1 на главной форме (эти буквы мы передадим в переменную ed чуть позже). Если текст в переменной s совпадает с текстом из поля Edit1, то переменной Accept присваивается True, и запись принимается. Иначе запись отфильтровывается. Не забудьте сохранить проект. Далее перейдем в главную форму. Нужно удалить весь текст из события onChange компонента Edit1, и вписать новый: {Изменение поиска по фамилии} procedure TfMain.Edit1Change(Sender: TObject); begin //если в поле Edit1 есть хоть одна буква, if Edit1.Text <> '' then begin fDM.TLichData.Filtered := False; //отключаем фильтр ed := Edit1.Text; //передаем в fDM новый текст fDM.TLichData.Filtered := True; //включаем фильтр end //если букв нет, фильтрацию отключаем: else fDM.TLichData.Filtered := False; end; Вот и все. Что же тут у нас происходит? Как только пользователь введет хоть одну букву, срабатывает событие onChange компонента Edit1. Если в Edit1 есть хоть одна буква, то мы вначале отключаем фильтрацию, отменяя прошлый фильтр, если он был. Затем мы передаем в глобальную переменную ed, расположенную в модуле данных, текст из Edit1. Далее снова включаем фильтр. При этом срабатывает событие onFilterRecord нашей таблицы, и в этом событии сравнивается текущее значение переменной ed и записей поля "Фамилия". Сохраните проект, скомпилируйте и запустите программу. Проверьте, как фильтруются записи. Имея воображение, в событии onFilterRecord можно устраивать сколь угодно сложные проверки. Ведь в этом событии можно сравнивать не одно поле, а несколько, причем поля не обязательно должны быть индексированы. Вы можете проверять на совпадение хоть все поля таблицы, и поскольку фильтрация происходит путем перебора записей, то усложнение условных проверок заметно не замедлит этот процесс. Использование индексов Создание индексных полей обеспечивает сортировку данных по этим полям, что также облегчает поиск данных - ведь найти нужную фамилию или имя проще, если они отсортированы по алфавиту. Причем имеется возможность сортировать записи не только по возрастанию, но и по убыванию, хотя в большинстве руководств по Delphi эта возможность не описывается. При создании в базе данных таблицы LichData мы указали поля "Фамилия" и "Имя", как индексированные. Этим и воспользуемся. Чтобы включить сортировку записей по полю "Фамилия", достаточно указать название поля в свойстве IndexFieldNames таблицы: fDM.TLichData.IndexFieldNames := 'Фамилия'; Если требуется отключить сортировку, этому свойству присваивается пустая строка: fDM.TLichData.IndexFieldNames := ''; Существует еще одна хитрость, о которой мало где можно прочитать. При индексировании таблицы к имени поля можно прибавить строку " ASC ", если мы желаем сортировать в возрастающем порядке (по умолчанию), или " DESC ", если сортируем в убывающем порядке. Сортировка " ASC " используется по умолчанию. Добавим возможность сортировки по фамилии и имени в нашу программу. Для этого на главную форму установим компонент TPopupMenu с вкладки Standard палитры компонентов. Дважды щелкните по компоненту, чтобы открыть редактор меню. Создадим следующие пункты: Сортировать по фамилии Сортировать по имени Не сортировать Обратная сортировка В редакторе меню выделите пункт "Сортировать по фамилии" и измените свойство Name этого пункта на NFam. Пункт "Сортировать по имени" переименуйте в NImya. Пункт "Не сортировать" - в NNet, а пункт "Обратная сортировка" - в NObrat. Вначале создайте обработчик событий для пункта "Не сортировать" (дважды щелкните по пункту). Тут все просто: {Не сортировать} procedureTfMain.NNetClick(Sender: TObject); begin fDM.TLichData.IndexFieldNames := ''; end; Для обработчика событий пункта "Сортировать по фамилии" код немного сложней: {Сортировать по фамилии} procedure TfMain.NFamClick(Sender: TObject); var stype :String; begin //выбираем направление сортировки: if NObrat.Checked then stype := ' DESC' //обратная сортировка elsestype := ' ASC'; //прямая сортировка //сортируем fDM.TLichData.IndexFieldNames := 'Фамилия' + stype; end; Здесь, в зависимости от состояния свойства Checked пункта "Обратная сортировка" мы присваиваем строковой переменной stype либо значение ' ASC ' (прямая сортировка), либо ' DESC ' (обратная сортировка). Обратите внимание, что перед строкой имеется пробел, он нужен, чтобы строка не "прилепилась" к названию поля. Далее мы устанавливаем индекс, указывая имя поля и добавляя к нему значение переменной stype. Таким образом, если Checked пункта "Обратная сортировка" имеет значение True (галочка установлена), мы добавляем ' DESC ', или ' ASC ' в противном случае. В результате имя индексного поля может быть либо "Фамилия ASC ", либо "Фамилия DESC ". Сортировку по имени кодируем аналогичным образом: {Сортировать по имени} procedure TfMain.NImyaClick(Sender: TObject); var stype :String; begin //выбираем направление сортировки: if NObrat.Checked then stype := ' DESC' else stype := ' ASC'; //сортируем fDM.TLichData.IndexFieldNames := 'Имя' + stype; end; Нам осталось указать код пункта всплывающего меню "Обратная сортировка". Тут нам нужно не просто установить галочку, если ее не было, но также проверить - есть ли сортировка по какому либо полю? Если таблица отсортирована, требуется ее пересортировать по этому же полю, но уже в обратном порядке. Вот код: {Команда "Обратная сортировка"} procedureTfMain.NObratClick(Sender: TObject); begin //изменяем направление сортировки NObrat.Checked := notNObrat.Checked; //если сортировка по фамилии, пересортируем ifPos('Фамилия',fDM.TLichData.IndexFieldNames)>0 then fMain.NFamClick(Sender); //если сортировка по имени, пересортируем if Pos('Имя',fDM.TLichData.IndexFieldNames)>0 then fMain.NImyaClick(Sender); end; Как видите, мы использовали функцию Pos(), которая возвратит ноль, если в строке не найдено указанной подстроки, или номер символа, с которого эта подстрока начинается, если она есть. Нам нужно определить, не входит ли в имя индексного поля "Фамилия" или "Имя". Ведь к имени поля добавлена строка ' ASC ' или ' DESC ', так что прямая проверка if fDM.TLichData.IndexFieldNames = 'Фамилия' then результата не даст, в любом случае результатом было бы False. Ну а для пересортировки мы вызываем соответствующий пункт меню, чтобы не писать код сортировки еще раз, например: fMain.NFamClick(Sender); Следует заметить, что при большом количестве записей в таблице смена индексного поля будет несколько замедлять работу приложения. Тем не менее, индексация таблицы - очень удобный и часто применяемый способ организации вывода записей. В свойстве PopupMenu верхней сетки DBGrid1 выберите созданное только что всплывающее меню, чтобы оно открывалось только над этой сеткой, сохраните проект, скомпилируйте его и опробуйте сортировку данных. Напоследок заметим, что мы имеем возможность применить одновременно и фильтрацию записей, и их индексацию. Это позволяет нам создать достаточно мощный и удобный для пользователя механизм поиска записей в нашей программе. Лекция 4. Наборы данных. Основные свойства, методы и события Цель: Ознакомление с наиболее востребованными свойствами, методами и событиями, общими для всех наборов данных, потомков класса TDataSet. Рассмотрить методов блокировки данных при использовании файл-серверной архитектуры, а также курсоров, специфичных для механизма ADO. До сих пор мы работали с таблицами с помощью компонента TADOTable. На самом деле мы работали не с самими таблицами, а с Набором данных (DataSet). Набор данных - это коллекция записей из одной или нескольких таблиц базы данных. Наборы данных можно получить с помощью компонент TADOTable, TADOQuery или TADOStoredProc, который необходим для архитектуры клиент-сервер. Каким образом получаются наборы данных? Когда мы открываем таблицу, то есть, присваиваем True свойству Active компонента TADOTable, например, специальный механизм делает выборку записей в соответствии с заданными параметрами, и возвращает нам эти записи в виде таблицы. Можно сказать, что наборы данных - это прослойка между нашим приложением и реальными таблицами, хранящимися в базе данных. Все указанные выше компоненты являются наборами данных, имеют общего предка - класс TDataSet и заимствовали от него свойства, методы и события, добавляя собственные возможности. Об этом и поговорим на этой лекции. Свойства Active - Свойство имеет логический тип и позволяет открыть или закрыть набор данных, если свойству присвоить True или False соответственно. В зависимости от свойства CanModify данные можно либо только просматривать, либо можно также редактировать их. AutoCalcFields - Свойство логического типа. Если установить значение False, то возникновение события OnCalcFields будет подавляться, вычисляемые поля обрабатываться не будут. Значение True разрешает расчет вычисляемых полей. Bof - Свойство имеет логический тип и содержит True, если курсор находится на первой записи набора данных, и False в противном случае. Bof содержит True, когда: Не пустой набор данных открывается. При вызове метода First. При вызове метода Prior, если курсор при этом на первой записи набора данных. При вызове метода SetRange в пустом наборе данных или диапазоне. Bookmark - Свойство позволяет установить закладку на текущей записи набора данных. Количество закладок может быть неограниченно, работа с закладками рассматривалась на курсе "Введение в программирование на Delphi ". Свойство имеет тип TBookmarkStr. CanModify - Свойство имеет логический тип, и показывает, можно ли редактировать полученный набор данных, или он доступен только для чтения. При открытии набора данных автоматически запрашивается доступ для редактирования. В таком доступе может быть отказано по разным причинам, например, таблица открыта другим пользователем в эксклюзивном режиме. В этом случае CanModify получает значение False, и мы можем только просматривать данные, но не вносить в них изменения. DatabaseName - Свойство строкового типа, содержит адрес базы данных или ее псевдоним. Однако это справедливо к наборам данных BDE. В случае использования механизма ADO, это свойство недоступно - вместо него для подключения к базе данных следует использовать свойство Connection или ConnectionString. DataSource - Свойство используется в наборах данных для указания детального набора данных в отношениях один-ко-многим. DefaultFields - Свойство логического типа, содержит True, если программист не создал ни одного поля в редакторе полей набора данных. В этом случае все поля определяются автоматически, в соответствии с данной таблицей. Eof - Свойство, противоположное свойству Bof. Имеет логический тип, и имеет значение True в случаях, когда: Открыт пустой набор данных. Вызван метод Last. Вызван метод Next, если указатель при этом находится на последней записи таблицы. При вызове метода SetRange в пустом наборе данных или диапазоне. FieldCount - Свойство целого типа, содержит количество полей в наборе данных. Fields - Свойство позволяет получить значение нужного поля по его индексу. Поля при этом индексируются с нуля. Например, получить значение седьмого по счету поля набора данных можно так: Edit1.Text := CustTable.Fields[6].Value; FieldValues - Свойство позволяет получить значение нужного поля по его имени. Это свойство используется по умолчанию, поэтому его можно не указывать. Примеры: Edit1.Text := CustTable. FieldValues ['Order']; Edit1.Text := CustTable['Order']; Filter - Свойство строкового типа. Содержит строку, которая определяет правила фильтрации набора данных. Filtered - Свойство логического типа. Если в свойстве Filter имеется строка, определяющая порядок фильтрации, то присвоение значения True свойству Filtered приводит к фильтрации набора данных. Присвоение этому свойству False отменяет фильтрацию. FilterOptions - Свойство имеет тип TFilterOptions и применяется для строковых или символьных полей. Свойству можно присвоить значение foCaseInsensitive или foNoPartialCompare. В первом случае фильтрация будет учитывать регистр букв, во втором учитывается лишь точное совпадение образцу. Modified - Очень важное свойство логического типа. Содержит True, если набор данных был изменен, и False в противном случае. Часто применяется для проверок: если набор данных изменен, то вызвать метод Post, чтобы сохранить изменения. RecNo и RecordCount - Свойства целого типа. Первое содержит номер текущей записи в наборе данных, второе - общее количество записей. State - Очень важное свойство, определяющее состояние набора данных. Может иметь следующие значения: dsInactivate - набор данных закрыт. dsBrowse - режим просмотра. dsEdit - режим редактирования. dsInsert - режим вставки. dsSetKey - поиск записи. dsCalcFields - состояние установки вычисляемых полей. dsFilter - режим фильтрации записей. dsNewValue - режим обновления свойства TField.NewValue. dsOldValue - режим обновления свойства TField.OldValue. dsCurValue - режим обновления свойства TField.CurValue. dsBlockRead - состояние чтения блока записей. dsInternalCalc - обновление полей, у которых свойство FieldKind соответствует значению fkInternalCalc. Методы Append - Метод добавляет новую запись в конец набора данных. При этом набор данных автоматически переходит в режим редактирования. AppendRecord(constValues: arrayofconst) - Метод добавляет новую запись в конец набора данных, и заполняет поля этой записи значениями из массива, переданного в метод как параметр. Cancel - Отменяет все изменения набора данных, если они еще не сохранены методом Post или переходом на другую запись. ClearFields - Метод очищает все поля текущей записи. Close - Закрывает набор данных. Метод является альтернативой присваивания False свойству Active набора данных. Delete - Метод удаляет текущую запись. Следует заметить, что во многих форматах данных удаляемая запись лишь помечается, как удаленная, и скрывается от пользователя. Физически же такая запись из файла не удаляется. В этом случае обычно время от времени приходится "паковать" таблицы, избавляясь от таких записей. Edit - Метод переводит набор данных в состояние редактирования. Если этого не сделать, изменение записи будет невозможным. FieldByName - Еще один способ получить значение поля или изменить его, указывая имя поля. При этом можно использовать явное преобразование данных в нужный тип, например, AsInteger, AsString и т.п. Пример: Table1. FieldByName ('QUANTITY').AsInteger := StrToInt(Edit1.Text); FindFirst ,FindLast , FindNext и FindPrior - Методы пытаются установить курсор соответственно, на первую, на последнюю, на следующую и на предыдущую запись. В случае успеха методы возвращают True. Переход к другой записи приводит к автоматическому сохранению изменений, если изменения были. First ,Last , Next и Prior - просто устанавливают указатель соответственно на первую, последнюю, следующую и предыдущую запись. Переход к другой записи приводит к автоматическому сохранению изменений, если изменения были. FreeBookmark - Метод освобождает память, связанную с закладкой Bookmark. Обычно вместо вызова этого метода достаточно присвоить закладке пустую строку (см. лекцию 30 курса "Введение в программирование на Delphi"). GotoBookmark - Метод обеспечивает переход на закладку Bookmark, переданную в качестве параметра. Insert - Метод вставляет новую запись в указанную в параметре позицию набора данных. При этом набор данных автоматически переходит в режим редактирования. InsertRecord(constValues: arrayofconst) - Метод вставляет новую запись в набор данных, и заполняет поля этой записи значениями из массива, переданного в метод как параметр. Пример: Customer. InsertRecord ([CustNoEdit.Text, CoNameEdit.Text, AddrEdit.Text, Null, Null, Null, Null, Null, Null, DiscountEdit.Text]); Обратите внимание, что в некоторые поля были вставлены значения Null, то есть, ничего. То же самое происходит, когда пользователь при редактировании записи вносит значения не во все поля. IsEmpty - Метод возвращает True, если в наборе данных нет записей. Применяется для проверки - не пуста ли таблица? Locate - Метод ищет запись в наборе данных (см. предыдущую лекцию). Lookup - Метод ищет запись в наборе данных (см. предыдущую лекцию). В отличие от Locate не переводит указатель на найденную запись, а лишь возвращает значения ее полей. Open - Метод открывает набор данных. То же самое происходит, если свойству Active набора данных присвоить значение True. Post - Метод сохраняет сделанные изменения в наборе данных. Refresh - Метод заново перечитывает таблицу и обновляет набор данных. Имеет смысл использовать в приложениях, где несколько пользователей работают с одной базой данных. События After … - События, возникающие после вызова соответствующего метода: AfterCancel - Событие возникает после отмены изменений в текущей записи. AfterClose - Событие возникает после закрытия набора данных. AfterDelete - Событие возникает после удаления текущей записи. AfterEdit - Событие возникает после перехода набора данных в режим редактирования. AfterInsert - Событие возникает после вставки новой записи. AfterOpen - Событие возникает после открытия набора данных. AfterPost - Событие возникает после вызова метода Post. AfterScroll - Событие возникает после перехода на другую запись. Before … - События, возникающие перед вызовом соответствующего метода: BeforeCancel - Событие возникает перед отменой изменений в текущей записи. BeforeClose - Событие возникает перед закрытием набора данных. BeforeDelete - Событие возникает перед удалением текущей записи. BeforeEdit - Событие возникает перед переходом набора данных в режим редактирования. BeforeInsert - Событие возникает перед вставкой новой записи. BeforeOpen - Событие возникает перед открытием набора данных. BeforePost - Событие возникает перед вызовом метода Post. BeforeScroll - Событие возникает перед переходом на другую запись. OnCalcFields - Событие возникает при необходимости переопределения вычисляемых полей. Такое событие возникает всякий раз, когда программа должна сформировать значения для вычисляемых полей. Событие возникает также при открытии набора данных, и при любом его изменении. Если алгоритм вычислений достаточно сложен, база данных большая, а пользователь интенсивно с ней работает, событие OnCalcFields может значительно замедлить работу с базой данных. В этом случае следует отключать это событие. Для этого достаточно присвоить значение False свойству AutoCalcFields текущего набора данных. OnFilterRecord - Событие возникает при включении фильтрации записей. OnNewRecord - Событие возникает при вызове методов Append или Insert. Блокировка таблиц в архитектуре файл-сервер При работе в архитектуре файл-сервер с единой сетевой базой данных работают несколько клиентских приложений. При этом нередко возникает ситуация, когда один пользователь вносит изменения в базу данных. В этот момент, во избежание потери или порчи данных, следует запретить внесение изменений другими пользователями. Такая блокировка достигается методом LockTable(). При этом значение свойства LockType этого набора данных определяет вид запрета. Значение ltReadLock запрещает чтение, а ltWriteLock - запись в набор данных. Можно запретить и чтение, и запись, но для этого следует вызвать метод LockTable () дважды. Блокировка таблиц методом LockTable () справедлива для таблиц Paradox или dBase, если вы используете механизм BDE. Когда изменения внесены, и необходимость блокировки пропадает, можно снять блокировку методом UnlockTable(), указав в параметре тип снимаемого запрета ( ltWriteLock, ltReadLock ). Свойство LockType набора данных ADO имеет тип TADOLockType: type TADOLockType = (ltUnspecified, ltOptimistic, ltBatchOptimistic); ltReadOnly, ltPessimistic, Это свойство позволяет определить тип блокировки при открытии набора данных. Как видно из описания типа, свойство может иметь следующие значения: ltUnspecified - тип блокировки не определен. ltReadOnly - блокировка записи, читать данные можно. ltPessimistic - пессимистическая блокировка. Свойство указывает, что если вы редактируете запись, то другие пользователи не смогут редактировать ее, пока вы не сохраните изменения. ltOptimistic - оптимистическая блокировка. Блокировка подразумевает, что возникновение конфликта маловероятно. В связи с этим любой пользователь в любое время может редактировать любую запись. Проверка на наличие конфликтов производится только в момент сохранения изменений. ltBatchOptimistic - свойство устанавливает блокировку на пакет записей, а не на отдельную запись. При этом все обновления, сделанные пользователем, не записываются сразу, а накапливаются в оперативной памяти. Позже они сохраняются одним пакетом. Такой подход увеличивает производительность приложения, но также увеличивается риск возникновения конфликтов. Курсоры в наборах данных ADO Наборы данных ADO имеют два специфичных свойства, неразрывно связанные друг с другом: CursorLocation и CursorType. Курсоры оказывают большое влияние на то, каким образом извлекаются данные из таблиц, каким образом вы можете перемещаться по ним и т.д. Фактически, курсор - это механизм перемещения по записям набора данных. От того, какой курсор используется в многопользовательской среде, зависит способ перемещения по записям: только вперед или в обе стороны. Будете ли вы видеть изменения, сделанные другими пользователями, также зависит от типа применяемого курсора. CursorLocation (положение курсора) Это свойство определяет, каким образом извлекаются и модифицируются данные. Значений только два: clUseClient - курсор на стороне клиента. clUseServer - курсор на стороне сервера. Клиентский курсор обслуживается механизмом ADOCursorEngine. В момент открытия набора данных все данные перекачиваются с сервера на клиентский компьютер. После этого данные хранятся в оперативной памяти. Перемещения по данным и их модификация происходит значительно быстрее, кроме того, клиентский курсор обладает более широкими возможностями. Серверный курсор обслуживается операционной системой. Благодаря тому, что курсор находится на стороне сервера, приложению нет смысла перекачивать все данные разом, это повышает скорость работы с БД. Серверные курсоры больше подходят для обслуживания больших наборов данных. Следует заметить, что если вы работаете с локальной базой данных (например, Access ), то серверный курсор будет обслуживаться программой, обслуживающей эту базу данных. CursorType (тип курсора) Имеется пять типов курсора: Unspecified - не указанный. В Delphi такой тип не используется, он присутствует только потому, что имеется в ADO. Forward-only (только вперед). Этот тип курсоров обеспечивает самую высокую производительность, однако он позволяет перемещаться по записям только в одном направлении - от начала к концу, что делает его малопригодным для создания пользовательского интерфейса. Однако он хорошо подходит для программных операций, таких как перебор записей, формирование отчета и т.п. Static(статический) - пользователь имеет возможность перемещаться в обоих направлениях, однако изменения записей, выполненные другими пользователями, не видны таким курсором. Keyset(набор ключей). При открытии набора данных с сервера читается полный список всех ключей. Этот набор ключей хранится на стороне клиента. Если приложение нуждается в данных, провайдер OLEDB читает строки таблицы. Однако после открытия набора данных в этот набор нельзя добавлять новые ключи, или удалять имеющиеся. То есть, если другой пользователь добавил новую запись, текущий клиент ее не увидит. Однако он увидит изменения существующих записей, сделанные другими пользователями. Dynamic(динамический). Самый мощный тип курсора, но при этом и самый ресурсоемкий. Он позволяет видеть все изменения, все добавления или удаления, сделанные другими пользователями, но при этом больше других замедляет работу с БД. Лекция 5. Таблицы Paradox в ADO Цель: Создание небольшое приложение с двумя таблицами Paradox. Это приложение понадобится в дальнейшем, для изучения свойств полей. В приложении для доступа к этим таблицам используем механизм ADO, для чего потребуется создать и настроить поставщика данных ODBC. Для таблиц будет использоваться связь один-комногим для создания подстановочного lookup поля. Подключение таблиц Paradox 7 к приложению через ADO Изучение свойств полей лучше сразу проводить на примере. Для этого создадим небольшую демонстрационную базу данных, всего из двух таблиц, и приложение, работающее с ней. Попутно затронем темы, которых раньше не касались. Цель проекта: создать мини-меню для столовой, кафе или ресторана. Прежде всего, определимся с таблицами. Таблицы будем создавать в формате Paradox 7, описание типов полей которого подробно рассматривалось на лекции №30 курса "Введение в программирование на Delphi ". Для доступа к данным этих таблиц используем механизм ADO. Создавать таблицы удобней с помощью утилиты DatabaseDesktop, входящей в состав Delphi. Пусть главная таблица называется Food, ее поля описаны в таблице 5.1: Таблица 5.1. Поля таблицы Food И мя Тип Описание поля: FKey Auto increment (+) Ключевое поле, служит счетчиком блюд. FName Alpha (A) Текстовое поле размером 30, название блюда. FType Long Integer (I) Поле служит для связи с подчиненной таблицей, в которой хранятся названия типов (супы, напитки, салаты и т.п.) FVeget Logical (L) Логическое поле - вегетарианская еда, или нет. Потребуется для изучения свойств логических полей. FCena Money ($) Поле денежного типа. Стоимость блюда. Подчиненная таблица будет еще проще: Таблица 5.2. Поля таблицы Tips Имя поля: Тип Описание TKey Auto increment Ключевое поле, служит счетчиком типов. (+) TName Alpha (A) Текстовое поле размером 20, название типов. Итак, начнем. Откройте утилиту DatabaseDesktop. Чтобы облегчить работу и не искать каждый раз нужный каталог, укажем сразу рабочую папку, которую нужно вначале создать средствами Windows: C:\Menu Для этого выберите команду меню "File ->WorkingDirectory ". В открывшемся окне нажмите кнопку " Browse " и найдите эту директорию на диске. Когда вы выберите ее, нажмите кнопку " OK ". Теперь эта папка стала папкой "по умолчанию". При попытке открыть или создать таблицу в утилите DatabaseDesktop, эта папка всегда будет текущей (если в дальнейшем вы не смените рабочую папку). Далее выбираем команду "File ->New ->Table". Оставьте тип Paradox 7, нажмите "ОК". Далее вам предлагается ввести названия и типы полей. Сделайте это, как в таблице 5.1. Как только вы ввели названия, типы и размеры (размер есть только у текстового поля), в списке " Tableproperties " выберите команду " TableLanguage " и нажмите кнопку " Modify ". В выпадающем списке выберите язык, как на рисунке 5.1: Рис. 5.1. Выбор языкового драйвера для таблицы Paradox Если этого не сделать, у вас будут проблемы с отображением русских символов. Далее нажмите кнопку " Saveas " и укажите имя таблицы: Food. Таким же образом сделайте таблицу Tips, руководствуясь таблицей 5.2. После этого вы можете закрыть утилиту DatabaseDesktop, она больше не нужна. Пойдем дальше. Поскольку мы собираемся подключаться к таблицам Paradox с помощью механизма доступа к данным ADO, нам потребуется установить на компьютере нужный драйвер ODBC. Для этого откройте Панель управления (Пуск -> Настройка -> Панель управления). Если вы используете Windows 2000, XP или более новую, вам придется еще выбрать команду "Администрирование". Далее открываем " Источники данных ODBC ". Нажимаем кнопку "Добавить", выбираем драйвер " MicrosoftParadoxDriver (*.db) " и нажимаем кнопку "Готово". Далее в поле "Имя источника данных" укажите MenuParadox, этот источник мы будем использовать только для этой нашей программы. Затем уберите галочку "Использовать текущий каталог" и нажмите кнопку "Выбор каталога". В открывшемся окне выберите нашу папку C:\Menu: Рис. 5.2. Установка драйвера ODBC Нажимаете "ОК", и драйвер готов. Теперь можете закрыть все остальные окна, они больше не нужны. Загружаете Delphi. Свойству Name главной формы присвойте имя fMain, сохраните модуль формы как Main, а проект в целом как MyMenu. В свойстве Caption формы напишите "Изучение свойств полей". На форму бросьте компонент Panel с вкладки Standard, свойство Align установите в alTop. Ниже с вкладки DataControls установите компонент DBGrid, в свойстве Align которого выберите alClient, чтобы заполнить оставшееся пространство. Затем на панель установите простую кнопку, в свойстве Caption которой напишите "Типы блюд". У вас должна получиться такая форма: Рис. 5.3. Главная форма проекта Раз у нас еще будет форма с типами блюд, следовательно, понадобится и модуль данных, общий для всех форм. Выберите команду File ->New ->DataModule. В свойстве Name модуля укажите fDM и сохраните модуль под именем DM. Теперь с вкладки ADO устанавливаем компонент ADOConnection. Сразу свойство Name для краткости обращения переименуйте в Con1. Займемся подключением. Дважды щелкните по компоненту, чтобы открыть редактор подключений. Нажмите кнопку " Build ". На вкладке "Поставщик данных" по умолчанию должен быть " MicrosoftOLEDBProviderforODBCDrivers ". Нам нужен именно этот поставщик. Переходим на вкладку "Подключение" (для этого можете просто нажать кнопку "Далее"). В выпадающем списке "Использовать имя источника данных" нам нужно выбрать MenuParadox, то подключение, которое мы создали ранее. К слову сказать, мы могли и не указывать адрес данных, могли оставить галочку "Использовать текущий каталог". В этом случае таблицы нужно было бы расположить там же, где и программа, или создавать подключение при работающей программе. Так удобно делать при использовании локальной базы данных. Способ, которым мы воспользовались сейчас, более удобен для многопользовательских файл-серверных БД. Если бы базы лежали где-то на сетевом диске, тогда мы могли бы в качестве папки указать сетевой путь, например: \\myserver\Menu но тогда эта папка должна быть открыта в сети как общий ресурс. Нажмите кнопку "Проверить подключение". Если вышло сообщение "Проверка подключения выполнена", значит, вы все сделали правильно, и ошибок нет. Нажимаем кнопку "ОК", чтобы подтвердить подключение, и еще раз "ОК", чтобы закрыть окно подключений. Сразу же свойство LoginPrompt компонента Con1 переводим в False, чтобы каждый раз при подключении программа не запрашивала имя пользователя и пароль. Затем в свойстве Connected устанавливаем True. Подключение произошло. Далее с вкладки ADO устанавливаем два компонента ADOTable. Выделите оба компонента, и в их свойстве Connection выберите нашCon1. Займемся вначале первой таблицей. В свойстве TableName выберите таблицу Food, свойство Name переименуйте в FoodT, а свойство Active переведите в True. Для второго компонента ADOTable выберите таблицу Tips, а компонент переименуйте в TipsT. Также переведите Active в True. Далее рядом с таблицами установите два компонента DataSource с вкладки DataAccess. Первый переименуйте в FoodDS, второй - в TipsDS. В свойстве DataSet каждого выберите соответствующую таблицу. Не забудьте сохранить проект. Перейдите на главную форму. Командой File ->Useunit подключитесь к созданному модулю данных. В свойстве DataSource сетки DBGrid выберите fDM.FoodDS. На сетке должны появиться столбцы с данными. Нажмите кнопку Run на панели инструментов или горячую клавишу F9. Проект компилируется, запускается, и… выходит ошибка: Рис. 5.4. Ошибка при компиляции В чем дело? Вроде бы, мы все делали правильно, иначе на сетке DBGrid не появились бы нужные столбцы? Просто мы добрались до проблем с полями. Нажмите кнопку "ОК", затем выберите Run ->Programreset, чтобы закрыть повисшую программу. Теперь перейдите на окно модуля данных, щелкните дважды по компоненту FoodT, чтобы вызвать редактор полей. Затем щелкните по окну редактора правой кнопкой и выберите команду Addallfields (Добавить все поля). То же проделайте и со второй таблицей. Снова сохраните проект, скомпилируйте его и запустите - теперь полный порядок, программа запускается и выполняется нормально. Дело в том, что, используя драйверы ODBC с "неродными" форматами баз данных, такими как dBase или Paradox, приходится точно указывать, какие у нас поля, а не надеяться на авто-определение. Вообще, создавать поля для каждого набора данных считается хорошим тоном в программировании. Далее создадим еще одну форму, для редактирования типов блюд: Рис. 5.5. Форма редактора типов блюд Форму назовите fMyTypes, сохраните модуль как MyTypes. Чтобы убрать из окна лишние кнопки системной строки и не позволять пользователю менять размеры окна, в свойстве BorderStyle формы выберите значение bsDialog. Не забудьте подключить к нему модуль данных DM. На форме две простых кнопки, поле DBEdit с вкладки DataControls и сетка DBGrid с этой же вкладки. В свойстве DataSource и сетки, и поля выберите fDM.TipsDS. У поля DBEdit, кроме того, в свойстве DataField выберите поле TName. Обратите внимание, что я назвал форму и модуль как MyTypes, а не просто как Types. Слово Types (типы) довольно распространенное в языках программирования и может вызвать конфликт названий. Попробуйте, если не верите! Дважды нажимаем на верхнюю кнопку, и в обработчике пишем код: //добавляем запись: fDM.TipsT.Append; //переводим фокус: DBEdit1.SetFocus; В коде обработки нижней кнопки просто закрываем окно: Close; Однако нам нужно убедиться, что если изменения в таблице были, и пользователь желает их сохранить, то они сохраняются. Поскольку мы не знаем точно, каким образом пользователь закроет это окно, придется для проверки сгенерировать событие onClose для формы: {если изменения есть, спросим что с ними делать.если пользователь не желает их сохранять, отменяем изменения. иначе сохраняем: } if fDM.TipsT.Modified then if Application.MessageBox('Данные изменены! Сохранить?', 'Внимание!', MB_YESNO+MB_ICONQUESTION) <> IDYES then fDM.TipsT.Cancel else fDM.TipsT.Post; Далее переходим на главную форму, командой File ->UseUnit подключаем модуль MyTypes, дважды щелкаем по кнопке "Типы блюд" и в сгенерированном событии вызываем новый модуль: fMyTypes.ShowModal; Сохраните проект, скомпилируйте его и впишите 5-10 типов блюд, например, "Напитки", "Супы", "Салаты" и т.п. Это нам будет нужно для подстановочного поля. Если в поле автоинкремента у вас будут выходить нули, не обращайте внимания - после сохранения таблицы там окажутся правильные цифры. Вы сможете убедиться в этом, закрыв программу и загрузив ее еще раз. Теперь займемся формой для редактирования основной таблицы. Командой File >New ->Form или аналогичной кнопкой на панели инструментов создайте новую форму. В свойство Caption этой формы впишите "Редактирование блюда", в свойстве Name укажите fEditor, а модуль сохраните как Editor. Сразу же командой File ->UseUnit подключите к этой форме модуль данных DM. Форма будет выглядеть так: Рис. 5.6. Форма редактора блюда Как видно из рисунка, на форме присутствуют поясняющие компоненты Label, три компонента DBEdit, один DBLookupComboBox, один DBNavigator и кнопка BitBtn, в свойстве Kind которой выбрано значение bkClose. Компонент DBLookupComboBox немного сложней остальных. Это подстановочный компонент. Из основной таблицы Food он будет брать целое число значение поля FType. А из дочерней таблицы Tips этот компонент будет просматривать все значения поля TName. Когда пользователь выберет какой-нибудь тип блюда, целое число, соответствующее ключевому полю TKey, попадет в поле FType главной таблицы. Другими словами, у нас получилась связь один-ко-многим (многие блюда основной таблицы могут иметь одинаковый тип): Рис. 5.7. Связь между таблицами Выделите все компоненты, относящиеся к редактированию данных или перемещению по ним (начинающиеся на DB …), и в их свойстве DataSource выберите fDM.FoodDS. Затем с помощью свойства DataField подключите все DBEdit к соответствующему полю таблицы. У компонента DBLookupComboBox установите следующие значения: Таблица 5.3 . Значения свойств компонента DBLookupComboBox Свойство Значение DataSource fDM.FoodDS DataField FType ListSource fDM.TipsDS KeyField TKey ListField TName Как видно из таблицы, компонент DBLookupComboBox имеет такие важные свойства: DataSource - свойство содержит ссылку на компонент TDataSource, связанный с основной таблицей. DataField - свойство указывает на имя ссылочного поля основной таблицы. В это поле после выбора значения из списка DBLookupComboBox попадает значение ключевого поля подстановочной таблицы. ListSource - свойство содержит ссылку на компонент TDataSource, связанный с подстановочной (дочерней) таблицей. KeyField - свойство содержит имя ключевого поля подстановочной таблицы. По этому полю ищется нужная подстановочная запись. ListField - свойство содержит имя поля подстановочной таблицы, по которому формируется список значений DBLookupComboBox. Эти значения также можно подставлять в основную таблицу в виде lookup (подстановочного) поля. Если говорить еще проще, то при открытии основного и подстановочного наборов данных, компонент DBLookupComboBox соединяется с подстановочной таблицей, указанной в свойстве ListSource, и формирует список значений из поля, указанного в ListField. Далее, при редактировании основной таблицы, пользователь выбирает из списка DBLookupComboBox одно из значений. При этом DBLookupComboBox смотрит, какое значение выбранной записи в подстановочной таблице имеет ключевое поле KeyField. Как правило, это целое число. Это число DBLookupComboBox и заносит в ссылочное поле основной таблицы DataField. Похожим образом действует и компонент DBLookupListBox, разумеется, учитывая специфику компонента. Форма fEditor предназначена для редактирования имеющегося блюда или добавления нового, в зависимости от того, каким способом вызвали форму. Поэтому здесь нам нужно лишь создать код для события закрытия формы onClose, куда пропишем: if fDM.FoodT.Modified then if Application.MessageBox('Данные изменены! Сохранить?', 'Внимание!', MB_YESNO+MB_ICONQUESTION) <> IDYES then fDM.FoodT.Cancel else fDM.FoodT.Post; Перейдем на главную форму. Командой File ->UseUnit добавим к главной форме новое окно. Пользователь должен иметь возможность редактировать имеющуюся запись, поэтому сгенерируем событие onDblClick для сетки DBGrid1, и пропишем туда следующий код: fEditor.ShowModal; Рядом с кнопкой "Типы блюд" добавим еще одну кнопку "Добавить блюдо". Сгенерируйте событие нажатия на эту кнопку и пропишите такой код: fDM.FoodT.Append; fEditor.ShowModal; Как видите, отличие кода заключается лишь в том, что при нажатии на кнопку добавляется новая запись, открывается редактор и пользователь редактирует ее. А если он дважды щелкнет по записи на сетке, то откроется тот же редактор, в который будет загружена текущая запись. Впрочем, благодаря навигатору DBNavigator, пользователь и там имеет возможность перемещаться по записям, добавлять или удалять записи. Мы, собственно, получили далекую от идеала, но вполне работоспособную программу. Сохраните проект, скомпилируйте и введите для примера несколько блюд: Рис. 5.8. Программа в действии Как видите, программа имеет множество недостатков: пользователь видит совершенно ненужные ему ключевые поля, в сетке он видит лишь номер типа блюда, но не видит название этого типа. В поле FVeget ему вручную приходится писать True или False вместо привычных Да/Нет. Еще недостаток: названия полей в сетке соответствуют названиям полей в таблице, а поле " FType " или " FVeget " мало что скажет пользователю. Исправлением этих недостатков займемся в следующей лекции, вместе с изучением свойств полей. Лекция 6. Поля (TField) Цель: Ознакомление с объектами-полями и классом TField. Научитесь создать и использовать в наборах данных подстановочные и вычисляемые поля , изучите наиболее важные свойства, методы и события класса TField. Каждая база данных состоит из таблиц, каждая таблица - из записей. Каждая запись в свою очередь представляет собой набор полей. Поле набора данных - это экземпляр достаточно мощного класса TField, о котором мы и поговорим на этой лекции. Изучение свойств полей будем проводить на примере приложения из прошлой лекции. Подстановочные (Lookup) поля Подстановочное поле Lookup изначально в набор данных не входит, его нужно создавать самостоятельно. Такое поле отличается от обычного тем, что показывает данные из другого набора данных. Для использования такого поля два набора данных обязательно должны иметь релятивную связь. На прошлой лекции мы применяли компонент DBLookupComboBox, который является аналогом подстановочного поля, но который нельзя показать в сетке DBGrid. При создании подстановочного поля также необходимо указать набор данных, откуда поле будет просматривать значения, ключевые поля для релятивной связи и поле со значениями, которые нужно подставлять. Открываем проект из "Таблицы Paradox в ADO", открываем окно модуля данных. Дважды щелкаем по компоненту FoodT, чтобы открыть редактор полей. В этом редакторе у нас уже присутствуют пять полей, имеющихся в таблице, добавим шестое, подстановочное. Для этого щелкните правой кнопкой по редактору полей и выберите команду NewField (Новое поле): Рис. 6.1. Создание подстановочного (Lookup) поля В разделе Fieldtype (Тип поля) вы можете выбрать один из трех вариантов. Нас сейчас интересует тип Lookup. Заполните необходимые поля значениями, как на рисунке 6.1 и нажмите кнопку "ОК". Новое подстановочное поле будет добавлено в набор данных. В списке полей его можно переместить мышью на другое место, установите его сразу под FName. Перейдите на окно главной формы и убедитесь, что новое поле появилось. Однако оно пока еще не содержит данных - данные будут доступны только во время прогона программы. Сохраните проект, скомпилируйте и посмотрите, как работает программа. Как мы видим, теперь на главной форме два поля, которые ни к чему показывать пользователю - FKey с номерами записей, и FType - с номерами типов блюд, которые нам уже не нужны, поскольку мы показываем сами типы. Уберем их, точнее, сделаем невидимыми. Снова откройте редактор полей набора данных FoodT. Установите свойство Visible этих полей в False. Вычисляемые (Calculated) поля Как и подстановочное, вычисляемое поле изначально не входит в набор данных, а добавляется в процессе проектирования приложения. Вычисляемые поля предназначены для показа данных, которые автоматически вычисляются в процессе работы программы, используя одно или несколько полей набора данных. К примеру, в таблице имеется поле стоимости товара и количество, которое купил какой-то клиент. Вычисляемое поле, перемножив значения этих полей, может показать общую стоимость товара. В нашем примере мы создадим вычисляемое поле для показа стоимости блюда в долларах США. Для этого в модуле данных создадим глобальную переменную dollar: var fDM: TfDM; dollar: Currency = 30.36; Вы можете указать текущий курс доллара к рублю, он так быстро меняется, что едва ли будет таким, как в моем примере. Итак, дважды щелкаем по набору данных FoodT, чтобы открыть редактор полей. Щелкаем правой кнопкой по этому редактору и выбираем команду " Newfield ". В поле " Name " впишите название нового поля FDCena. В поле " Component " автоматически отобразится имя нового объекта-поля " FoodTFDCena ", по которому в дальнейшем мы сможем к нему обращаться. Это имя составное - имя набора данных плюс имя нового поля, без всяких пробелов и разделителей. В поле " Type " выберите тип Float, так как у нас могут быть копейки, вернее, центы. Затем убедитесь, что переключатель установлен на " Calculated " и нажмите "ОК". В редакторе полей появилось новое поле. Чтобы мы не получили сумму с кучей цифр после запятой, выделите в редакторе полей поле FDCena, и в его свойстве DisplayFormat укажите маску "#.## $US" (разумеется, без кавычек). К слову сказать, при создании вычисляемого поля мы могли бы выбрать тип Currency (денежный), но тогда к цифре добавлялось бы "р.", если вашаWindows имеет российские настройки. Вещественные поля набора данных наряду с полями целого типа имеют четыре свойства, которые могут вам пригодиться: DisplayFormat - Определяет формат отображения числа. DisplayEdit - Определяет формат числа при редактировании. MaxValue - Определяет максимально возможное для поля число. MinValue - Определяет минимально возможное число. Свойства MaxValue и MinValue по умолчанию имеют значение 0, что указывает на отсутствие ограничений. Однако, это еще полдела. Поле мы сделали, осталось сделать вычисления. Код необходимых вычислений прописывается в свойстве OnCalcFields набора данных. Закройте редактор полей и выделите НД FoodT. Сгенерируйте для него обработчик события OnCalcFields и в этом обработчике пропишите следующую строчку: FoodTFDCena.Value := FoodTFCena.Value / dollar; Как видно из примера, мы используем значения одного или нескольких полей текущего набора данных, производим над ними какие то вычисления, и результат этих вычислений присваиваем вычисляемому полю. Сохраните проект, скомпилируйте и посмотрите, как работает программа. Если вы все сделали правильно, у вас получится подобная картина: Рис. 6.2. Подстановочное и вычисляемое поле в программе Поле данных (Data) Если вы помните, при создании нового поля мы имеем три переключателя Fieldtype. Переключатель " Data " предназначен для создания поля данных - пустого поля, которое программист использует по своему усмотрению. Наполнение этого столбца можно прописать в обработчике события OnGetText полученного объекта-поля. На практике такие поля используют редко, чаще они применяются для программного создания таблиц, о чем мы поговорим на одной из следующих лекций. Свойство DisplayValues Свойство DisplayValues объекта-поля предназначено для отображения данных логического поля в нужном формате. Для примера изменим отображение данных поля FVeget. Откройте редактор полей компонента FoodT, выделите поле FVeget. В его свойстве DisplayValues укажите значение "Да;Нет". У логического поля вместо True и False здесь можно указать свою пару значений. Значение до точки с запятой считается истинным, значение после - ложным. Примеры: Да;Нет Муж;Жен Yes;No Y;N Д;Н и т.п. Указанные в свойстве значения пары "Истина;Ложь" будут отображаться в компонентах вывода данных, таких как DBGrid, DBEdit и т.п. Кроме того, эти же значения будут отображены, если вы будете получать значения этого поля с использованием свойства AsString, чтобы преобразовать значение в строковый тип. Для облегчения пользователю ввода данных немного изменим проект. Откройте окно fEditor и удалите DBEdit, предназначенный для ввода логического значения в поле FVeget. Вместо него установите компонент DBComboBox. Дважды щелкните по свойству Items этого компонента и в открывшемся редакторе значений впишите две строки: Да Нет В свойстве DataSource компонента выберите таблицу FoodT, а в свойстве DataField - поле FVeget. Сохраните проект, скомпилируйте и посмотрите, как он работает. Теперь пользователю не нужно вписывать значение - он может выбрать его из списка. Для полей других типов свойство DisplayValues недоступно. Вместо него предлагается использовать свойство DispalyFormat, которое доступно только для числовых полей и полей типа TDataTime. При этом формат задается так же, как в функциях формата, например, FormatFloat() и FormatDateTime(), применение которых подробно рассматривалось на курсе "Введение в программирование на Delphi ". Например, для поля типа Дата формат: ddddddmmmyyyy выведет дату в формате "Понедельник 04 Янв 2010" Другие наиболее важные свойства класса TField Aligment - Определяет выравнивание выводимого значения.Может иметь следующие значения: taLeftJustufy - выравнивание по левому краю taRightJustify - выравнивание по правому краю taCenter - выравнивание по центру. AsXXXX - Группа свойств этого типа преобразует значение поля к нужному типу.Вместо XXXX могут быть использованы: BCD - двоично-десятичный тип. Boolean - логический тип. Currency - денежный тип. DataTime - тип дата-время. Float - вещественный тип. Integer - целый тип. String - строка. Variant - variant. Пример: DBText1.Field.AsString := 'Santa Cruz Wharf'; Calculated - Содержит True, если значение поля OnCalcFields набора данных, и False в противном случае. CanModify - Содержит True, если значение поля противном случае. Currency - Свойство доступно у вещественных проектировании приложения присвоить True, то значения формате. DataSize - Содержит размер данных. вычисляется в обработчике можно изменить, и False в полей. Если свойству при будут выходить в денежном DataType - Содержит тип данных, определяемый перечислением TFieldType, например, ftString - строка, ftBoolean - логический тип, ftFloat - вещественный тип, и так далее. Класс TFieldType содержит достаточно большой список типов полей, более подробные данные вы можете посмотреть в справочной системе Delphi. DisplayLabel - Позволяет ввести строку - заголовок отображаемого столбца. Если заголовок не задан, по умолчанию будет использоваться имя поля. EditMask - Позволяет указать строку - маску для ввода данных. FieldName - Имя поля. Lookup - Содержит True, если поле подстановочное. Origin - Содержит имя поля в физической таблице. ReadOnly - Если содержит True, значение поля нельзя менять. Required - Если содержит True, значение поля не может быть пустым. Size - Если поле имеет запись переменной длины, свойство указывает текущий размер данных. Value - Содержит значение поля. Visible - Если содержит True (по умолчанию), поле отображается в таких компонентах, как DBGrid. Наиболее важные методы класса TField AssignValue() - Преобразует вариантное значение поля Value с помощью метода AsXXXX и помещает результат в переменную Value, переданную в метод как параметр. Create() - Создает поле-объект и инициализирует его. Destroy() - Уничтожает поле-объект. Наиболее важные события класса TField OnChange - возникает после изменения данных поля и их успешной записи. OnGetText - в обработчике этого события можно подготовить текст для свойств DisplayText и Text. OnSetText - возникает при записи данных из параметра Text в свойство Text. OnValidate - возникает после изменения значения но до записи в буфер. Этот обработчик удобно использовать для проверки на правильность введенных данных. Обращение к значению поля К значению поля можно обратиться через свойства Value или AsXXXX, например: Edit1.Text := FoodTFName.Value; Edit1.Text := FoodTFName.AsString; Применение свойства AsXXXX приводит к преобразованию значения в нужный тип. Разумеется, типы должны быть совместимыми. Например, целое число можно преобразовать в вещественное, но не наоборот. Если вы не вызывали редактор полей и не создавали для набора данных ни одного объекта-поля, то значение поля можно получить через свойство FieldByName, например: FoodT.FieldByName('FName').AsString := Edit1.Text; Кроме того, доступ к значению поля можно получить через свойства набора данных Fields или FieldValues: FoodT.Fields[1].AsString := Edit1.Text; FoodT.FieldValues['FName'] := Edit1.Text; Как уже упоминалось, свойство FieldValues в наборах данных применяется по умолчанию, так что последний пример можно записать и так: FoodT['FName'] := Edit1.Text; Если для доступа к полю вы используете свойство Fields, имейте в виду, что индексация полей начинается с 0, то есть индекс 1 соответствует второму полю набора данных. Свойство FieldValues обладает еще одной особенностью: оно имеет вариантный тип и позволяет использование списка полей, таким образом, единственным оператором можно записать сразу несколько полей: var v :Variant; begin //создаем вариантный массив: v := VarArrayCreate([0, 2], varVariant); //читаем значения полей: v := FoodT['FName;FType;FCena']; Edit1.Text := v[0]; Edit2.Text := v[1]; Edit3.Text := v[2]; Лекция 7. Запросы Цель: Ознакомление с компонентом-запросом TADOQuery, написать приложение SQLмонитор и испытывать несколько операторов SQL, формируя различные наборы данных. Изучение свойства и методы компонентов-запросов, отличные от других наборов данных. Запросы (TQuery, TADOQuery) Запросы (TQuery, TADOQuery) - это такие же наборы данных, как и таблицы ( TTable, TADOTable ). Запросы, как и таблицы, происходят от общего предка - TDBDataSet, в связи с этим они имеют схожие свойства, методы и события. Но имеются и существенные различия. Прежде всего, если табличный набор данных TTable( TADOTable ) получает точную копию данных из таблицы базы данных, то запрос TQuery ( TADOQuery ) получает этот набор, основываясь на запросе, сделанном на специальном языке SQL (StructuredQueryLanguage - Язык Структурированных Запросов). С помощью этого языка программист создает запрос, который передается параметру TQuery.SQL( TADOQuery.SQL ). При открытии набора данных этот запрос обрабатывается используемым механизмом BDE, ADO или др. и в набор данных передаются запрошенные данные. Заметили разницу? Не копия таблицы, а именно запрошенные данные, причем в указанном порядке! Используя запросы, в одном наборе данных можно получить взаимосвязанные данные из разных физических таблиц. Отпадает надобность в подстановочных полях. Имеется два варианта работы с SQL -запросами. В первом случае, SQL -запрос запрашивает нужные данные из таблицы (таблиц) базы данных. При этом формируется временная таблица, созданная в каталоге запуска программы, и компонент-запрос становится ее владельцем. Работа с такими данными очень быстрая, но пользователь при этом не может изменять данные, он лишь просматривает их. Такой подход идеален для составления отчетности. Если же пользователю требуется вносить изменения в таблицу (таблицы), то с помощью специальных операторов SQL( INSERT, UPDATE, DELETE ) формируется запрос, уведомляющий механизм доступа к данным изменить данные БД. В этом случае никаких временных таблиц не создается. Запрос передается механизму доступа, обрабатывается им, выполняются изменения, и механизм доступа уведомляет программу о благополучном (или нет) изменении данных. Сравним табличные наборы данных с запросами. При работе с локальными или файл-серверными БД, табличные наборы данных имеют преимущество в скорости доступа к данным, поскольку запросы создают и используют для этого временные таблицы, а табличные НД напрямую обращаются к физическим таблицам БД. Однако при этом, запросы позволяют формировать гибкие наборы данных, которые невозможно было бы получить с помощью табличных НД. При работе в архитектуре клиент-сервер, всякое преимущество табличных наборов данных пропадает. Ведь они должны получить точную копию запрошенной таблицы, в которой могут быть десятки и сотни тысяч записей. Если учесть, что все эти данные передаются по сети, и передаются не одному, а множеству клиентов, то мы получим очень медленную систему, постоянно перегружающую сеть. Компонент TADOQuery Для демонстрации работы компонента TADOQuery создадим совсем маленькое приложение - простейший SQL -монитор (у Delphi имеется встроенный SQL -монитор, но ведь всегда приятно сделать что-то своими руками!). Итак, создайте папку для нового приложения. В эту папку скопируйте базу данных ok.mdb, с которой мы работали в четвертой лекции. Если вы еще помните, там у нас имеется четыре таблицы, предназначенные для программы отдела кадров. Создайте новый проект в Delphi, форму переименуйте в fMain, сохраните ее модуль под именем Main, а проект в целом как SQLMon. В свойстве Caption формы пропишите "Простой SQL монитор". Далее на форму установите панель. В свойстве Align панели выберите alTop, чтобы панель заняла весь верх, а ее высоту растяните примерно на полформы. Очистите свойство Caption. На эту панель установите компонент Memo, именно в нем мы будем писать наши SQL -запросы. Дважды щелкните по свойству Lines этого компонента, чтобы вызвать редактор текста, и очистите весь текст. Также не помешает дважды щелкнуть по свойству Font и изменить размер шрифта на 12 для лучшего восприятия текста. В свойстве Align выберите alLeft, чтобы компонент Memo занял всю левую часть панели. В правой части панели установите две простые кнопки и компонент TDBNavigator с вкладки DataControls панели инструментов. Для улучшения внешнего вида интерфейса ширину кнопок сделайте такой же, как у навигатора базы данных. В свойстве Caption первой кнопки напишите "Выполнить SQL -запрос", на второй кнопке напишите "Очистить компонент Memo ". Собственно, мы могли бы очищать Memo сразу при выполнении SQL -запроса, и обойтись без второй кнопки. Но многие запросы похожи, и проще изменить часть текста запроса, чем писать весь запрос заново. На нижнюю, свободную половину формы установите компонент TDBGrid с вкладки DataControls для отображения данных. В свойстве Align сетки выберите alClient, чтобы сетка заняла все оставшееся место. У вас должна получиться такая картина: Рис. 7.1. Внешний вид приложения Еще нам потребуются три компонента: TADOConnection и TADOQuery с вкладки ADO для получения набора данных, и TDataSource с вкладки DataAccess для связи сетки DBGrid и навигатора DBNavigator с этим набором данных. Дважды щелкните по ADOConnection1, чтобы вызвать редактор подключений. Нажмите кнопку " Build ", выберите поставщика MicrosoftJet 4.0 OLEDBProvider, и нажмите "Далее". В поле "Выберите или введите имя базы данных" укажите нашу БД ok.mdb и нажмите "ОК". И еще раз "ОК", чтобы закрыть окно редактора подключений. Сразу же свойство LoginPrompt переводим в False, чтобы при каждом запуске программы у нас не запрашивался логин и пароль, а Connected в True. Подключение к базе данных произошло. В свойстве Connection компонента TADOQuery выберем ADOConnection1, а в свойстве DataSet компонента DataSource1 выберем наш НД ADOQuery1. Теперь набор данных ADOQuery1 соединен с базой данных, а DataSource1 - с этим набором данных. В свойстве DataSource компонентов DBGrid1 и DBNavigator1 выберем DataSource1, чтобы они могли взаимодействовать с набором данных. Нам осталось лишь запрограммировать обработчик события onClick для обеих кнопок. Щелкните дважды по кнопке "Выполнить SQL -запрос", чтобы сгенерировать это событие, и пропишите в нем такой код: //проверим - есть ли текст в Memo. Если нет, выходим: if Memo1.Text = '' then begin ShowMessage('Вначале введите запрос!'); Memo1.SetFocus; Exit; end; //текстесть.Очистим предыдущий запрос в наборе данных: ADOQuery1.SQL.Clear; //добавим новый запрос из Memo: ADOQuery1.SQL.Add(Memo1.Text); //открываем набор данных, т.е. выполняем запрос: ADOQuery1.Open; Комментарии здесь достаточно подробны, чтобы разобраться в происходящем. Заметим только, что набор данных ADOQuery1 обычно закрыт. После того, как мы изменяем его свойство SQL, прописывая туда новый SQL -запрос, этот набор данных открывается. В результате в БД передается SQL -запрос, получаются запрашиваемые данные, которые формируют набор данных ADOQuery1. Когда этот компонент активен, данные доступны. Можно также заполнить свойство SQL, дважды щелкнув по нему и открыв редактор запросов, и сделать активным во время проектирования программы. Тогда данные становятся доступны сразу. Такой подход удобен, когда программист не собирается в дальнейшем менять SQL -запрос этого набора данных. Однако чаще бывает наоборот - в зависимости от ситуации, используется то один, то другой запрос в одном и том же наборе данных. Так мы поступаем и в нашем примере - передача SQL -запроса и открытие набора данных мы будем делать программно. Еще мы можем заметить, что свойство SQL набора данных TADOQuery имеет тип TStrings, так же, как свойство Lines компонента Memo или свойство Items компонента ListBox. То есть, в свойстве SQL мы можем использовать все преимущества, которые нам дает тип TStrings, например, загрузка SQL -запроса из внешнего файла: ADOQuery1.SQL.LoadFromFile('c:\myfile.sql'); Подобный прием нередко используется программистами, когда нужно сделать программу более гибкой. Формируя файл с SQL -запросами можно получать различные наборы данных, в зависимости от обстоятельств. Но в нашей программе мы будем получать SQL -запрос из поля Memo. Поскольку тип TStrings используется и в Memo, и в ADOQuery, то следующие строки кода аналогичны, они одинаково сформируют SQL запрос на основе текста в поле Memo: ADOQuery1.SQL.Add(Memo1.Text); ADOQuery1.SQL := Memo1.Lines; Сгенерируйте событие нажатия на вторую кнопку, здесь мы должны просто очистить поле текста Memo1, и код совсем прост: Memo1.Clear; Вот и вся программа! Сохраните ее, скомпилируйте и запустите программу на выполнение. В поле Memo впишите следующие строки: SELECT * FROMLichData; После этого нажмите кнопку "Выполнить SQL -запрос". В сетке DBGrid отобразятся данные, которые представляют собой точную копию таблицы LichData из базы данных ok.mdb. Строки в примере написаны по правилам и рекомендациям языка SQL, то есть, операторы пишутся заглавными буквами, каждый оператор на отдельной строке, а в конце ставится точка с запятой. Однако рекомендации можно нарушать, а правила в Delphi более мягкие. Так, мы можем написать весь текст маленькими буквами, в одну строку, не ставить точку с запятой и не обращать внимания на регистр букв: select * fromlichdata Запрос все равно будет выполнен. Однако лучше придерживаться рекомендаций и традиционного синтаксиса SQL, ведь этот язык имеет стандарты, и вы можете применять его не только при работе с Delphi. В других языках программирования или в клиентсерверных СУБД правила могут несколько отличаться, но в любом случае запрос, написанный в стандартном стиле, будет выполнен. Поэтому лучше сразу приучать себя к стандартному синтаксису. На данном курсе мы будем придерживаться рекомендаций SQL. Что же написано у нас в этом запросе? Оператор SELECT означает "выделить", звездочка означает "все поля", оператор FROM означает "из…". Таким образом, запрос означает: ВЫДЕЛИТЬ все поля ИЗ таблицы LichData Но такой запрос ничем не отличается от применения табличных компонентов, а ведь мы можем создавать и гораздо более сложные запросы! Предположим, нам нужно получить фамилию, имя и отчество сотрудника, а также город его проживания. Основные данные находятся в таблице LichData, а вот город находится в таблице Adres, связанной с таблицей LichData релятивной связью один-к-одному по полю "Ключ" таблицы LichData, и по полю "Сотрудник" таблицы Adres. В этом случае запрос будет выглядеть так: SELECT Фамилия, Имя, Отчество, Город FROMLichData, Adres WHERE Ключ = Сотрудник; Как видите, в операторе SELECT поля перечисляются через запятую. Также через запятую перечисляются используемые таблицы в операторе FROM. А вот оператор WHERE указывает, что нужны только те записи, в которых значения поля "Ключ" и "Сотрудник" равны. Если бы мы не использовали оператор WHERE, то получили бы кучу недостоверных записей, где к каждой записи одной таблицы добавлялись бы все записи другой. Оператор WHERE позволил нам получить связные данные, в которых к одной записи первой таблицы добавляется соответствующая запись из другой таблицы. С этими и другими операторами мы подробней познакомимся на следующей лекции. Теперь предположим, что в одном наборе данных нам нужно получить записи из двух таблиц, связанных релятивной связью один-ко-многим. Так, у одного сотрудника может быть несколько телефонов. В этом случае придется смириться, что некоторые данные будут продублированы. Например, запрос: SELECT Фамилия, Имя, Телефон FROMLichData, Telephones WHERE Ключ = Сотрудник; выдаст нам набор данных, в котором фамилия и имя сотрудника будут дублироваться для каждого номера его телефона. Компонент-запрос может формировать набор данных двух типов: изменяемый, в котором пользователь может менять (редактировать, удалять или добавлять) записи, и не изменяемый, предназначенный только для просмотра данных или для составления отчетности. Возможность получения "живого" набора данных зависит от разных факторов - от применяемого оператора, от механизма доступа к данным, от используемой клиентсерверной СУБД. В данном примере мы используем оператор SELECT, работаем с локальной БД посредством механизма ADO. Если вы воспользуетесь навигатором, то убедитесь, что записи можно добавлять и удалять, а в сетке DBGrid их можно редактировать. Однако при редактировании данных, полученных более чем из одной таблицы, могут возникнуть трудности. Зато набор данных из одной таблицы можно спокойно изменять. Подробней с "живыми" и неизменяемыми наборами данных мы познакомимся позднее. Компонент TQuery/TADOQuery может выполнять запросы двумя разными способами. Вначале в свойство SQL компонента помещается необходимый запрос. Это можно сделать программно, как в нашем SQL -мониторе, так и на этапе проектирования приложения. Дальнейшие действия зависят от того, какой запрос нам нужно выполнить. Если это запрос на получение набора данных, то есть, оператор SELECT, то достаточно просто открыть TQuery/TADOQuery методом Open, или присвоив True свойству Active. Если же запрос должен модифицировать данные, то есть, используются такие операторы, как INSERT, UPDATE, DELETE, то тогда запрос выполняется методом ExecSQL. С работой компонента-запроса TQuery( TADOQuery ) мы поработали на практике. Как и табличные компоненты, компонент-запрос произошел от родительского класса TDBDataSet. Унаследовав его свойства, методы и события, он имеет и собственные, отличительные черты. Так, например, запрос может быть изменяемым (живым), при котором пользователь может модифицировать записи набора данных, и не изменяемым, при котором данные доступны только для просмотра и составления отчетности. Наиболее важные свойства, методы и события, отличные от TDBDataSet, рассматриваются ниже. Свойства компонента-запроса Constrained - Свойство логического типа. Если свойство имеет значение True, то в изменяемом наборе данных на модифицируемые записи накладываются ограничения блока WHERE оператора SELECT (с операторами SQL -запросов вплотную познакомимся на следующей лекции). DataSource - Указывает тот компонент TDataSource, который используется для формирования параметрического запроса. Local - Свойство логического типа. Если свойство имеет значение True, это означает, что компонент-запрос работает с локальной или файл-серверной базой данных. ParamCheck - Логическое свойство. При значении True список параметров автоматически обновляется при каждом программном изменении SQL -запроса. Params - Свойство имеет тип TParams и содержит массив объектов-параметров этого типа. На этом типе данных следует остановиться подробнее: Таблица 7.1. Свойства и методы типа TParams Свойство Описание Items Содержит массив параметров типа TParams и является свойством "по умолчанию". Индексация массива начинается с 0. ParamValues() Открывает доступ к значению параметра по его имени, указанному в скобках. Count Количество параметров в массиве. Метод Описание AddParam() Добавляет параметр в массив параметров. CreateParam() Создает параметр и добавляет его к массиву. FindParam() Ищет параметр по его имени, указанному в скобках. RemoveParam() Удаляет параметр из массива. Prepared - Свойство логического типа. Содержит значение True, если SQL -запрос был подготовлен методом Prepare. RequestLive - Логическое свойство. Если компонент-запрос содержит изменяемый (живой) набор данных, то RequestLive содержит True. RowsAffected - Свойство содержит количество записей, которые были удалены или отредактированы в наборе данных в результате выполнения SQL -запроса. SQL - Свойство типа TStrings, то есть, набор строк. Содержит SQL -запрос, который выполняется, как только компонент-запрос становится активным (открывается). При изменении этого свойства, компонент-запрос автоматически закрывается, так что программисту требуется перевести свойство Active набора данных в True (или вызвать метод Open ), чтобы запрос выполнился, и в НД появились запрошенные данные. Помещать строки запроса в свойство SQL можно как при проектировании, так и программно. В случае если программист создает запрос и открывает набор данных при проектировании приложения, он имеет возможность создать объекты-поля (см. предыдущую лекцию), и настраивать их свойства по своему усмотрению. При программном формировании НД такой возможности у него нет. UniDirectional - Свойство логического типа. Содержит True, если курсор набора данных может перемещаться только вперед (типы курсоров см. в лекции №4). Это свойство используется, в основном, при работе с клиент-серверными СУБД, не поддерживающими курсоры, которые могут двигаться как вперед, так и назад. Методы компонента-запроса ExecSQL() - Выполняет модифицирующие запросы, то есть запросы на изменение, добавление или удаление записей, а также создание или удаление таблиц. В случае обычных запросов, выполненных с помощью оператора SELECT, используется метод Open, или присвоение значения True свойству Active набора данных. ParamByName() - Метод дает доступ к значению параметра по его имени, указанному в скобках. Prepare() - Метод используется для передачи SQL -запроса механизму доступа к данным, чтобы последний оптимизировал запрос. Оптимизация запроса происходит следующим образом: при выполнении любого запроса, механизм доступа к данным проверяет его синтаксис, что отнимает некоторое время. В случае многократного применения запроса, его можно выполнить методом Prepare(). При этом запрос компилируется и запоминается в буфере. При повторном выполнении этого запроса его синтаксис уже не проверяется. UnPrepare() - Этот метод отменяет результаты действия метода Prepare(), и освобождает буфер от хранения компилированного запроса. Лекция 8.Краткий курс языка запросов SQL Цель: Ознакомление с основными командами языка запросов SQL и научить формировать как простые, так и сложные запросы. SQL( StructuredQueryLanguage ) - Это Язык Структурированных Запросов. Он не такой богатый, как языки программирования высокого уровня. Тем не менее, это язык, без владения которым программисту, работающему с базами данных, не обойтись. Запросы, написанные на SQL, часто называют скриптами. Как вы уже знаете, эти скрипты можно непосредственно вводить в свойство SQL компонента-запроса в момент проектирования приложения, а можно значение этого свойства менять и в процессе прогона программы. Однако нередко используют и третий способ: программист создает набор скриптовых файлов, в процессе работы программа считывает из них SQL -инструкции в компоненты запросов и выполняет их. Это простые текстовые файлы, созданные в любом редакторе текстов, например, стандартном Блокноте Windows. Расширение может быть любым, но традиционно используется *.sql. Все это позволяет создавать гибкие программы. Если организации, использующей ваше приложение, в дальнейшем потребуются какие-то новые возможности, например, им нужно дополнительно создать еще один отчет, то применение скриптовых файлов избавит вас от необходимости переделывать программу, для этого достаточно будет написать скрипт. В этой лекции мы разберем работу основных операторов SQL, после чего вы сможете создавать простые и сложные запросы и получать необходимые наборы данных. Тем, кто пожелает расширить свои познания SQL, рекомендую пройти соответствующий курс, посвященный этому языку, или прочитать книгу М. Грубера "Понимание SQL ". Книга описывает стандартный синтаксис языка SQL и затрагивает все его возможности. Команда SELECT Команда SELECT является основой запроса. Большинство SQL -запросов начинаются с нее. Множество других команд вкладываются в блок SELECT. Полный синтаксис этой команды таков: SELECT * | { [ DISTINCT | ALL] <value expression>.,..} FROM { <table name> [ <alias> ] }.,.. [ WHERE<predicate>] [ GROUP BY { <column name> | <integer> }.,..] [ HAVING<predicate>] [ ORDER BY { <column name> | <integer> }.,..]; Здесь используются следующие элементы: Таблица 8.1 . Элементы команды SELECT Элемент Описание Выражение, которое производит значение. Оно может включать имена <value столбцов. expression> <table name> Имя или синоним таблицы или представления Временный синоним для <tablename>, определенный в этой таблице и <alias> используемый только в этой команде. <predicate> Условие, которое может быть верным или неверным для каждой строки или комбинации строк таблицы в предложении FROM. Имя столбца в таблице. <column name> Число с десятичной точкой. В этом случае, оно показывает <integer> <valueexpression> в предложении SELECT с помощью идентификации его местоположения в этом предложении. В простейшем случае применение команды SELECT выглядит так: SELECT * FROMTable_Name; Звездочка указывает, что нужно показать все поля. Вместо звездочки можно указать конкретное поле или поля, разделяя их запятыми. Иногда бывает, что требуются данные из разных таблиц, которые имеют поля с одинаковым именем. В этом случае, перед именем полей указывают имя таблицы, или ее псевдоним, разделяя имена таблицы и поля точкой: SELECT Field1, Table1.Field2, Table2.Field2… FROMTable1, Table2; Команда FROM определяет имена таблиц, из которых осуществляется выборка данных. Если таблиц несколько, их имена разделяются запятыми. Иногда таблицы имеют длинные имена. В этом случае бывает выгодно использовать псевдонимы (alias) имен таблиц, указывая их через пробел после имени таблицы: SELECT Field1, f.Field2, s.Field2 FROM Table1 f, Table2 s; Команда WHERE Команда WHERE позволяет использовать условие, которые может быть верным или нет для каждой записи БД. Если условие верное, то запись добавляется в набор данных, иначе отвергается. Давайте рассмотрим пример. Загрузите SQL -монитор из прошлой лекции. Предположим, нам нужно получить следующие данные на каждого сотрудника: Фамилия, Имя, Отдел, Должность. Пишем соответственный SQL -запрос: SELECT Фамилия, Имя, Отдел, Должность FROMLichData, Doljnost; Выполнив этот запрос, вы получите нечто непонятное. В полученном наборе данных всем сотрудникам подряд присваивается вначале первая должность, затем вторая, и так до конца. Другими словами, если у вас 10 сотрудников и 10 должностей, то вместо ожидаемых десяти записей вы получите 10 * 10 = 100 записей! Полученные данные называют недостоверными. Чтобы избежать этого, существует команда WHERE, которая позволяет задать условие выборки данных: SELECT Фамилия, Имя, Отдел, Должность FROMLichData, Doljnost WHERE Ключ = Сотрудник; Теперь все в порядке, отдел и должность соответствуют каждому сотруднику. Мы указали, что нам нужны лишь те записи, значения которых в поле "Ключ" одной таблицы соответствуют значениям в поле "Сотрудник" другой таблицы. Полный синтаксис требует указания таблицы вместе с полем: WHERELichData.Ключ = Doljnost.Сотрудник; Однако поскольку у нас в этих таблицах нет полей с похожими именами, мы можем воспользоваться упрощенным вариантом. И в том, и в другом случае мы получим одинаковый набор данных. Как и в любом условии, здесь можно применять различные операторы сравнения: Таблица 8.2 . Операторы сравнения Оператор Описание = Равно > Больше < Меньше >= Больше или равно <= Меньше или равно <> Не равно Кроме того, мы можем использовать логические операторы AND, OR и NOT, формируя более сложные запросы: SELECT Фамилия, Имя, Отдел, Должность FROMLichData, Doljnost WHERE (LichData.Ключ = Doljnost.Сотрудник) AND (Должность = "Бухгалтер"); Логические операторы имеют более высокий приоритет, поэтому в приведенном примере можно обойтись и без скобок. Данный запрос выдаст нам данные только на бухгалтеров. Как вы могли заметить, в отличие от Delphi, строка в SQL заключается не в одинарные, а в двойные кавычки! Однако SQL более демократичен, одинарные кавычки тоже принимаются. Обычно их используют, если внутри строки требуется указать кавычки, например, 'Строка "в кавычках" будет отображена'. Еще следует заметить, что подобное связывание таблиц не требует наличия индексных полей. Но если такие поля есть, механизм доступа к данным будет их использовать для более эффективной выборки данных. Команда ORDERBY Команда ORDERBY позволяет сортировать записи по определенному полю как в возрастающем, так и в убывающем порядке. Воспользуемся предыдущим примером, и отсортируем записи по полю "Фамилия": SELECT Фамилия, Имя, Отдел, Должность FROM LichData, Doljnost WHERE Ключ = Сотрудник ORDERBY Фамилия; Как уже говорилось, мы можем сортировать данные как по возрастанию ( ASC ), так и по убыванию ( DESC ) значений. Сортировка по возрастанию установлена "по умолчанию", а вот чтобы сортировать записи по убыванию, после имени поля следует поставить служебное слово DESC: ORDERBY Фамилия DESC; Опять заметим, что для сортировки записей наличие индексных полей необязательно. Часто бывает, что нужно вывести данные из двух таблиц, имеющих связь один-ко-многим. При этом нужно сортировать данные не по одному, а по двум полям: SELECT Фамилия, Имя, Телефон, Примечание FROMLichData, Telephones WHERE Ключ = Сотрудник ORDERBY Фамилия, Телефон; В этом случае мы получим набор данных, в котором записи отсортированы вначале по фамилии сотрудника, затем по его номеру телефона: Рис. 8.1 . Двойная сортировка данных Оператор IN Оператор IN позволяет определить набор значений. Предположим, нам нужны сотрудники, проживающие в городах Москва и Санкт-Петербург. Мы можем сформировать сложный запрос: SELECT Фамилия, Имя, Город FROMLichData, Adres WHERE Ключ = Сотрудник AND (Город = "Москва" OR Город = "Санкт-Петербург"); Последнюю строку запроса можно упростить, если использовать оператор IN: WHERE Ключ = Сотрудник AND Город IN ("Москва", "Санкт-Петербург"); Представьте, если нужны данные не по двум, а по десятку городов. В какого бы монстра превратился запрос со сложным условием, если не использовать IN! При перечислении строк можно использовать как двойные, так и одинарные кавычки, при перечислении числовых значений кавычки не нужны. Все значения разделяются запятой. Оператор BETWEEN Оператор BETWEEN работает примерно так же, как IN, но задает не список, а диапазон значений. Предположим, нам нужно выявить сотрудников, которые имеют стаж работы от 4 до 10 лет включительно. Подобный запрос выглядит так: SELECT Фамилия, Имя, Стаж FROM LichData WHERE Стаж BETWEEN 4 AND 10; Оператор LIKE Оператор LIKE работает только с символьными и строковыми полями. Этот оператор позволяет находить записи, имеющие заданную подстроку. Предположим, нам требуется вывести всех сотрудников, чья фамилия начинается на букву "Л". Запрос будет таким: SELECT Фамилия, Имя, Отчество FROMLichData WHERE Фамилия LIKE 'Л%'; Следует учитывать, что оператор LIKE чувствителен к регистру букв. Если вы будете производить поиск записи в программе при помощи SQL -запроса, позаботьтесь заранее привести буквы к нужному регистру. Оператор LIKE использует маску символов, что позволяет задавать довольно сложные условия. Маска может иметь два специальных символа: " _ " - Символ подчеркивания обозначает, что в этом месте должен быть любой символ. Например, "м_р" может выводить такие слова, как "мир", "мор" или "мур", но не сможет вывести слово "мера". " % " - Символ процента обозначает, что в этом месте может быть любое количество любых символов. Например, маска '_и_и%' выведет такие фамилии, как Лисичкин, Синичкин, Милиев. Агрегатные функции Агрегатные функции используются в запросах SQL, чтобы из группы записей сформировать одиночное значение одного поля. Имеются следующие агрегатные функции: AVG - Функция возвращает среднее арифметическое значение из всех значений данного поля. Предположим, нам требуется выяснить средний стаж всех сотрудников предприятия. Такие данные могут быть сформированы следующим запросом: SELECT AVG (Стаж) FROM LichData; MAX - Функция возвращает максимальное значение указанного поля. Синтаксис аналогичен функции AVG. MIN - Функция возвращает минимальное значение указанного поля. Синтаксис аналогичен функции AVG. SUM - Функция возвращает сумму значений в указанном поле. Синтаксис аналогичен функции AVG. COUNT - Функция возвращает общее количество строк, сформированных запросом. В нашем случае это количество будет равно количеству сотрудников. Однако так называемые NULL -строки, то есть строки без значения, функция не учитывает. Если у какого-то сотрудника нет указания стажа его работы, то COUNT вернет меньшее количество, чем имеется сотрудников на предприятии. Синтаксис аналогичен функции AVG. С помощью агрегатных функций можно формировать более сложные значения. Предположим, у нас есть таблица покупателей. В ней указан покупатель, код приобретенного товара, его стоимость и количество. Если требуется, например, вычислить общую сумму проданного товара, то можно выполнить запрос: SELECTSUM (Стоимость * Количество) FROMPokupateli; В скобках функции указаны нужные параметры. Вначале для каждой записи вычисляется значение, в нашем случае, значение поля "Стоимость" умножается на значение поля "Количество". Таким образом, мы находим общую сумму, которую заплатил данный покупатель. Обойдя все поля таблицы, получим все суммы всех покупателей, после чего функция SUM вернет нам общую сумму, полученную от всех покупателей. В выражении агрегатных функций может участвовать любое количество полей таблицы, а само выражение может быть сколь угодно сложным, содержать различные арифметические операции, иметь вложенные скобки и т.д. Команда GROUPBY Команда GROUPBY позволяет группировать записи по какому-то определенному значению, и применяется совместно с агрегатными функциями. Предположим, нам требуется не просто получить средний стаж всех сотрудников, а еще и разбить эти данные по отделам. Вдруг директору приспичит узнать, в каком отделе у него работает больше всего молодых или старых специалистов! В нашу задачу входит выявить средний стаж сотрудников для каждого имеющегося отдела. Это мы можем выявить таким запросом: SELECTAVG(Стаж), Отдел FROMLichData, Doljnost WHERE Ключ = Сотрудник GROUPBY Отдел; Как видите, команда GROUPBY используется после команды WHERE и группирует записи по значению поля "Отдел". В результате получим таблицу из двух полей. Первое поле будет сформировано агрегатной функцией, второе поле "Отдел" из таблицы Doljnost. Для каждого отдела будет рассчитано среднее значение поля "Стаж". Команда GROUPBY позволяет группировать записи не только по одному, но и по множеству полей. Предположим, одну и ту же должность могут иметь несколько человек (например, пять бухгалтеров в бухгалтерии). Нам требуется найти самого старого сотрудника не только по отделу, но и по занимаемой должности. Нас выручит следующий запрос: SELECT Отдел, Должность, MAX(Стаж) FROMLichData, Doljnost WHERE Ключ = Сотрудник GROUPBY Отдел, Должность; В результате мы получим набор данных, сгруппированный не только по отделам, но и по должностям. Если должность занимает только один человек, то мы получим его стаж. Если несколько - то наибольший из них. Команда DISTINCT | ALL Команда DISTINCT (Отличие) предназначена для удаления избыточных (дублирующих) данных. Предположим, нам нужно получить список отделов на предприятии. Мы можем воспользоваться запросом: SELECT Отдел FROMDoljnost; Однако поле "Отдел" не является уникальным в этой таблице, то есть, если в отделе работает десяток специалистов с разными должностями, то мы получим десяток повторяющихся записей. Команда DISTINCT позволяет убрать такие избыточные данные: SELECTDISTINCT Отдел FROMDoljnost; В результате мы получим все названия имеющихся отделов, но эти названия не будут повторяться. Обратной командой является ALL, принятая по умолчанию. Если вы не используете DISTINCT, то автоматически используется ALL (то есть, показываются все записи). Команда HAVING Команда HAVING позволяет определить условия, чтобы удалить определенные группы из полученного набора данных, точно так же, как команда WHERE делает это для отдельных записей. Предположим, нам нужно получить максимальный стаж работы по каждой должности, как это мы делали выше, но при этом указать, что этот максимальный стаж должен быть более 7 лет. Следовательно, если на какой-то должности работают молодые сотрудники, имеющие меньший стаж работы, эта должность не будет приниматься. Для задания условия мы обычно используем команду WHERE, но в этой команде нельзя использовать агрегатные функции, формирующие значение из группы записей. Другими словами, запрос, подобный этому: SELECT Отдел, Должность, MAX(Стаж) FROM LichData, Doljnost WHERE Ключ = Сотрудник AND (MAX(Стаж) > 7) GROUPBY Отдел, Должность; вызовет ошибку. Использование в запросе команды HAVING решает эту проблему: SELECT Отдел, Должность, MAX(Стаж) FROM LichData, Doljnost WHERE Ключ = Сотрудник GROUPBY Отдел, Должность HAVINGMAX(Стаж) > 7; Мы изучили все основные команды SQL -запросов. На основе этих команд можно создавать запросы любой сложности. Однако еще раз заметим, что хорошие знания языка SQL позволят вам создавать более гибкие и мощные приложения, так что не ленитесь, изучайте SQL. Ведь вам наверняка придется создавать клиент-серверные базы данных, а в этой архитектуре вся работа с данными осуществляется только посредством SQL. Лекция 9. Приемы создания и модификации таблиц программно Цель: Ознакомление с тремя способами создания таблиц программно. Создать как простую, так и сложную индексированную таблицу, научитесь создавать таблицы через SQL-запрос. На прошлых лекциях мы изучили немало способов создания и обработки таблиц. В большинстве случаев, база данных и таблицы в ней проектируются и создаются заранее, например, такими утилитами, как DatabaseDesktop, или СУБД MSAccess. Однако иногда приходится создавать таблицы программно, то есть, не во время проектирования приложения, а во время его работы. Предположим, каждый пользователь программы должен иметь свою собственную таблицу в базе данных, или даже свою собственную БД. Сделать это заранее программист не может, ведь неизвестно, сколько пользователей на одном ПК будут работать с программой, и какие у них будут имена. К сожалению, в большинстве учебной литературы информация о программном создании таблиц или вовсе отсутствует, или очень скудна - описывается только какой то один способ создания только одного типа таблиц. А ведь программисту, в зависимости от обстоятельств, может понадобиться создание разного типа таблиц, разными способами. В данной лекции мы попробуем восполнить этот пробел и разберем три способа программного создания как простых, так и индексированных таблиц. BDE. Простая таблица. Наиболее простой способ создания таблицы без индексов предлагает механизм доступа к данным BDE. Плюсы данного способа в простоте выполнения, в возможности создания таблиц как текстового типа, так и dBase, Paradox или FoxPro. Суть данного способа заключается в предварительном создании объектов-полей в редакторе полей компонента TTable. Это также означает, что еще на этапе проектирования можно настроить формат объектов-полей по своему усмотрению. Рассмотрим этот способ на примере. Создайте новое приложение. Форму как всегда назовите fMain, сохраните модуль под именем Main, а проект в целом назовите как угодно. На форму установите простую панель, очистите ее свойство Caption, а свойству Align присвойте значение alTop. В левой части панели установите рядом две простые кнопки, а в правой - компонент TDBNavigator с вкладки DataControls Палитры компонентов. Ниже панели установите сетку TDBGrid, в ее свойстве Align выберите значение alClient. У кнопок измените свойство Caption: на первой кнопке напишите "Создать таблицу", на второй - "Открыть таблицу". Также нам потребуется еще четыре не визуальных компонента. Прямо на сетку, или в любое другое место установите компонент TTable с вкладки BDE, компонент TDataSource с вкладки DataAccess, и компоненты TSaveDialog и TOpenDialog с вкладки Dialogs. Подготовим диалоговые компоненты. Выделите их и присвойте свойству Filter обоих компонентов строку Таблицы dBase|*.dbf Таким образом, мы указали, что диалоги будут работать только с таблицами типа dBase. Кроме того, у обоих диалогов измените свойство DefaultExt, указав там: dbf Это свойство указывает расширение файла по умолчанию, если пользователь не назначит расширения сам. В свойстве DataSet компонента DataSource1 выберите таблицу Table1. В свойстве DataSource сетки DBGrid1 и навигатора DBNavigator1 выберите имеющийсяDataSource1. Теперь при открытии таблицы она будет отображаться в сетке, а навигатор позволит управлять ей. Теперь сложнее - настраиваем компонент Table1. Табличный компонент TTable имеет одно важное свойство TableType, с которым раньше нам не приходилось сталкиваться; компонент TADOTable такого свойства не имеет. Это свойство указывает на тип используемой или создаваемой таблицы. Свойство может иметь следующие значения: Таблица 9.1 . Значения свойства TableType компонента TTable Значение Описание Таблица содержится в формате обычного текстового файла. Строки и поля ttASCI разделяются специальными символами - разделителями. Имя файла таблицы имеет расширение *.TXT ttDBase Таблица содержится в формате dBase, файл по умолчанию имеет расширение *.DBF ttDefault Компонент определяет тип таблицы по расширению имени файла таблицы. При создании таблицы, если не указано расширение имени файла, принимается тип Paradox. ttFoxPro Таблица содержится в формате FoxPro, файл по умолчанию также имеет расширение *.DBF ttParadox Таблица содержится в формате Paradox, файл по умолчанию имеет расширение *.DB Если выбран тип таблицы (не ttDefault ), то будет использован этот тип вне зависимости от расширения указанного имени файла таблицы. В свойстве TableType компонента Table1 выберите значение ttDBase, то есть, таблица будет работать только с типом dBase. Далее дважды щелкните по компоненту, открыв редактор полей. Нам нужно будет добавить запланированные ранее поля. Щелкните по редактору правой кнопкой, выберите команду NewField (Новое поле). В поле Name впишите имя поля, например, FCeloe. В поле Type выберите тип поля Integer. В поле Size нужно указывать размер поля, но это справедливо только для текстовых полей и полей типов Memo или BLOB. Убедитесь, что переключатель FieldType установлен на Data, это создаст пустое поле указанного типа. Нажав кнопку "ОК" добавьте объект-поле в редактор полей. Таким же образом создайте еще несколько разнотипных полей. Каждому полю присвойте уникальное имя (ведь в таблице не может быть двух полей с одинаковым именем!). Важно, чтобы вы добавляли только те типы полей, которые поддерживаются выбранным типом таблиц, в нашем случае это dBase. При добавлении типа Memo укажите размер от 1 до 255, например, 50. В этом случае в файле таблицы *.dbf будет сохранен текст поля в 50 символов. Текст, который не уместится в этот размер, будет сохранен в файле Memo с таким же именем, но с расширением *.dbt. Делать табличный компонент активным на этапе проектирования не нужно. Итак, не имея базы данных, не имея физической таблицы, мы заранее установили тип таблицы и нужные нам поля. Как вы, наверное, догадываетесь, мы также имеем возможность сразу настроить нужные нам форматы для каждого поля, изменяя такие его свойства, как DisplayFormat, EditMask, DisplayLabel и др. Далее нам осталось непосредственно создать и открыть таблицу. Дважды щелкните по кнопке "Создать таблицу", сгенерировав для нее событие. В процедуру этого события впишите код: //если пользователь не выбрал таблицу, выходим: if not SaveDialog1.Execute then Exit; //закроем таблицу, если вдруг уже есть открытая: Table1.Close; //вначале устанавливаем адрес базы данных: Table1.DatabaseName := ExtractFilePath(SaveDialog1.FileName); //теперь устанавливаем имя таблицы: Table1.TableName := SaveDialog1.FileName; //физически создаем таблицу: Table1.CreateTable; //и открываем ее: Table1.Open; //запишем имя открытой таблицы: fMain.Caption := 'Таблица - '+ Table1.TableName; Комментарии к каждой строке достаточно подробны, чтобы вы самостоятельно разобрались с кодом. Метод CreateTable() компонента-таблицы создает файл таблицы, и дополнительные файлы ( Memo, индексные), если они нужны. В свойстве DatabaseName табличного компонента вы можете установить любой необходимый вам адрес, мы использовали папку, выбранную диалогом SaveDialog. Для кнопки "Открыть таблицу" код будет почти таким же: //если пользователь не выбрал таблицу, выходим: if not OpenDialog1.Execute then Exit; //закроем таблицу, если вдруг уже есть открытая: Table1.Close; //вначале устанавливаем адрес базы данных: Table1.DatabaseName := ExtractFilePath(OpenDialog1.FileName); //теперь устанавливаем имя таблицы: Table1.TableName := OpenDialog1.FileName; //открываем таблицу: Table1.Open; //запишем имя открытой таблицы: fMain.Caption := 'Таблица - '+ Table1.TableName; Откомпилировав программу и поработав с ней, вы обнаружите, что можете создавать и открывать сколь угодно много таблиц программно. При этом на каждую таблицу создается по два файла (если вы используете поле Memo ). Попробуйте таким же образом создать таблицу типа Paradox. BDE. Таблица с ключом и индексами. В задачу данного раздела входит создание таблицы Paradox с различными типами полей, с первичным ключом и индексами по текстовому полю как в возрастающем, так и в убывающем порядке. Редактор полей компонента TTable при этом вызывать не нужно, добавлять поля мы тоже будем программно. В целях экономии места проектирование формы приложения не описывается - это несложная задача. Вы можете создать главную форму такой же, как в предыдущем примере, только кнопка там будет одна. При нажатии на эту кнопку мы должны открыть таблицу, если она существует, или создать и открыть новую таблицу. Располагаться таблица должна в той же папке, откуда запущено приложение. Файл с таблицей Paradox назовем Proba.db, файлы с Memo и индексные файлы сгенерируются автоматически, также с именем Proba, но с разными расширениями. На форму добавьте компонент TTable с вкладки BDE, свойству Name которого присвойте значение TMy (вместо Table1 ), а свойству TableType значение ttParadox. Если у вас в приложении есть сетка DBGrid и (или) навигатор DBNavigator, то добавьте также компонент DataSource, который необходимо подключить к таблице TMy, а сетку и навигатор - подключить к DataSource. Здесь следует иметь в виду одну деталь: описание методов создания полей и индексов хранится в модуле DBTables, который подключается к вашей форме сразу, как вы установите компонент TTable. Если же вы используете модуль данных, и устанавливаете табличный компонент там, то и создавать таблицу нужно тоже в этом модуле, а в главной форме лишь вызывать процедуру создания таблицы. Но в нашем простом примере модуля данных нет, модуль DBTables указан в разделе Uses главной формы, и никаких проблем возникнуть не должно. Код нажатия на кнопку выглядит так: {Если таблицы нет - создаем и открываем ее, если естьпросто открываем} procedure TfMain.Button1Click(Sender: TObject); begin //если таблица есть - открываем ее и выходим: if FileExists(ExtractFilePath(Application.ExeName) + 'Proba.db') then begin TMy.DatabaseName := ExtractFilePath(Application.ExeName); TMy.TableName := 'Proba.db'; TMy.Open; Exit; end; //if {Если дошли до этого кода, значит таблицы еще нет. Указываем данные таблицы:} TMy.DatabaseName := ExtractFilePath(Application.ExeName); TMy.TableType := ttParadox; TMy.TableName := 'Proba'; {Создаем поля:} with TMy.FieldDefs do begin //вначале очистим: Clear; //добавляем поле-счетчик типа автоинкремент: with AddFieldDef do begin Name := 'Key'; DataType := ftAutoInc; Required := True; end; //with //добавляем текстовое поле: with AddFieldDef do begin Name := 'Name'; DataType := ftString; Size := 30; end; //with //добавляем поле дата: with AddFieldDef do begin Name := 'Date'; DataType := ftDate; end; //with //добавляем логическое поле: with AddFieldDef do begin Name := 'MyLog'; DataType := ftBoolean; end; //with //добавляем целое поле: with AddFieldDef do begin Name := 'MyInt'; DataType := ftInteger; end; //with //добавляем вещественное поле: withAddFieldDefdobegin Name := 'MyReal'; DataType := ftFloat; end; //with //добавляем денежное поле: with AddFieldDef do begin Name := 'MyCurr'; DataType := ftCurrency; end; //with //добавляем поле Memo: with AddFieldDef do begin Name := 'MyMemo'; DataType := ftMemo; Size := 20; end; //with end; //with {Создаем ключ и индексы:} with TMy.IndexDefs do begin Clear; //делаем первичный ключ: with AddIndexDef do begin Name := ''; Fields := 'Key'; Options := [ixPrimary]; end; //делаем индекс в возрастающем порядке: with AddIndexDef do begin Name := 'NameIndxASC'; Fields := 'Name'; Options := [ixCaseInsensitive]; end; //делаем индекс в убывающем порядке: with AddIndexDef do begin Name := 'NameIndxDESC'; Fields := 'Name'; Options := [ixCaseInsensitive, ixDescending]; end; end; //with //создаем таблицу: TMy.CreateTable; //и открываем ее: TMy.Open; end; Разберем приведенный код. Первый блок выполняет проверку на наличие таблицы. Таблица ищется в папке, откуда была запущена программа. Если таблица найдена, то компоненту TMy присваиваются свойства DatabaseName (папка, где располагается таблица) и TableName (имя таблицы). В нашем случае таблица называется Proba.db, но вы можете усложнить программу, используя диалог OpenDialog, как в прошлом примере. В этом случае пользователь сможет выбрать не только имя таблицы, но и ее расположение. Далее таблица открывается, а оператор Exit досрочно завершает выполнение процедуры. Если выполнение процедуры продолжается, значит, таблица не была найдена. В этом случае мы заполняем свойства компонента-таблицы DatabaseName, TableType и TableName необходимыми значениями. Далее начинаем добавлять поля. Чтобы уменьшить код, мы используем оператор with. Напомню, что этот оператор создает блок кода, который относится к указанному в with объекту. Так, вместо withTMy.FieldDefsdobegin Clear; можно было бы написать TMy.FieldDefs.Clear; В случае одиночного оператора это допустимо, но в случае множественных команд ссылаться в каждой строчке на объект будет утомительно. Свойство FieldDefs таблицы содержит описание полей этого набора данных. Таким образом, мы начинаем с того, что очищаем это описание. Далее у нас идет метод AddFieldDef, предназначенный для добавления поля в описание. Опять же, чтобы не ссылаться каждый раз на этот метод, мы используем вложенный оператор with для каждого добавляемого поля. В простейшем случае в блоке добавления нового поля требуется указать только два свойства объекта-поля: Name (имя поля) и DataType (тип поля). С именем все понятно, а что касается типа поля, то он определяется свойством DataType класса TField. Чтобы получить подробную справку по возможным типам полей, установите курсор в редакторе кода на слове DataType и нажмите <Ctrl+F1>, чтобы вызвать контекстную справку. В списке тем выберите ту тему, которая относится к классу TField, а в открывшейся справке щелкните по ссылке TFieldType (относится к Delphi 7, хотя возможно, имеется и в предыдущих версиях). Откроется страница с подробным описанием типов полей. При использовании этого метода следует сверяться, имеется ли выбранный тип поля в таблицах используемого формата. Помимо этих двух свойств, при необходимости могут использоваться и другие: Required - Логическое свойство. Если равно True, то значения поля должны быть уникальными (не могут повторяться). В нашем примере такое свойство имеется у поля, которое мы будем использовать как первичный ключ. Size - Указывает размер поля. Используется в основном, со строковыми и Memo полями. После того, как в список полей были добавлены все необходимые поля, начинаем создание первичного ключа и индексов. Если за список полей отвечает свойство FieldDefs таблицы, то за список индексов отвечает свойство IndexDefs, а за добавление нового индекса - метод AddIndexDef. По аналогии с полями, используем оператор with для уменьшения кода. Для каждого индекса требуется указать по три свойства: Name (имя индекса), Fields (имя поля, по которому строится индекс) и Options (параметры индекса).Параметры индекса указаны в таблице 9.2: Таблица 9.2. Параметры типов индекса Тип Описание Первичный индекс (ключ). Не применяется с таблицами типа dBase. ixPrimary Уникальный индекс. Значения этого поля не могут повторяться. ixUnique Индекс в убывающем (обратном) порядке. ixDescending Ключевой индекс для таблиц dBase. ixExpression ixCaseInsensitive Индекс, нечувствительный к регистру букв. ixNonMaintained Этот тип используется редко. Он подразумевает, что при редактировании пользователем значения индексируемого поля, индексный файл автоматически не обновляется. Как видно из примера, свойству Options можно присвоить не один параметр, а список параметров: Options := [ixCaseInsensitive, ixDescending]; Далее все просто: указав необходимые поля и индексы, методом CreateTable формируются физические файлы таблицы. Сама таблица имеет расширение *.db, файл с полем Memo - *.mb, остальные файлы содержат созданные индексы. Для сортировки данных используем индексы. У нас их два -' NameIndxASC ' (в возрастающем порядке) и ' NameIndxDESC ' (в убывающем порядке). Чтобы сортировать данные, например, в убывающем порядке, нужно указать имя соответствующего индекса в свойстве IndexName компонента-таблицы: TMy.IndexName := 'NameIndxDESC'; Если же мы хотим снять сортировку, то достаточно просто присвоить этому свойству пустую строку: TMy.IndexName := ''; Описываемый выше пример взят из справочника Delphi и приведен с небольшими доработками. Пример описывает практически все аспекты создания таблицы; по аналогии вы сможете создавать таблицы любой сложности. ADO. Создание простой таблицы посредством запроса SQL Создание таблицы выполняется SQL -запросом CREATETABLE. Тут есть одно но: дело в том, что существует два типа SQL -запросов. Запрос, который возвращает набор данных и начинается оператором SELECT, выполняется простым открытием компонентазапроса. При этом выполняется запрос, который содержится в свойстве SQL компонента. С модифицирующими командами дело обстоит иначе. Команда CREATETABLE принадлежит к той части SQL, которая называется DDL (DataDefinitionLanguage) - Язык Определения Данных. Этот язык предназначен для изменения структуры базы данных. Команды INSERT, DELETE, UPDATE относятся к DML (DataManipulationLanguage) Язык Обработки Данных, предназначенный для модификации данных. Эти команды объединяет то, что они не возвращают результирующий набор данных. Чтобы выполнить эти команды, нужно присвоить соответствующий SQL -запрос свойству SQL, а затем вызвать метод ExecSQL. Синтаксис создания таблицы несложный: CREATETABLE<TableName> (<ColumnName><DataType> [<Size>], …) Здесь, TableName - имя таблицы; ColumnName - имя столбца; DataType - тип данных и Size - размер, который указывается для некоторых типов данных, например, строки. Описания столбцов таблицы разделяются запятыми. В различных СУБД синтаксис и типы данных SQL могут отличаться. Поэтому запрос, прекрасно работающий в одной СУБД, может вызвать ошибку в другой. Чтобы избежать ошибок, рекомендуется везде использовать типы ANSI, являющиеся стандартом SQL. Увы, но этих типов очень немного. Рассмотрим их: Таблица 9.3 . Типы ANSI Тип Описание CHAR (CHARACTER) Строковые типы данных. Обычно имеют размер до 255 символов. Требуют указания размера. TEXT Целое число. Размер не указывается. INT (INTEGER) Короткое целое. Размер не указывается. SMALLINT Вещественные числа. Размер не указывается. FLOAT REAL Как видите, многих типов просто нет. Вместо логического типа, вероятно, придется использовать строковый тип с размером в один символ; при этом 'Y' или '1' будут означать истину, а 'N' или '0' - ложь. Программисту придется самостоятельно делать проверку на это значение. Нет типа Memo. Нет автоинкрементного типа. Однако стандартные типы непременно будут корректно работать в любой СУБД. Ниже приведен пример создания и открытия простой таблицы. В приложении должен иметься компонент ADOQuery, а если есть сетка и навигатор, то и DBSource. Для подключения к нужному провайдеру данных желательно использовать компонент TADOConnection. В его свойство ConnectionString нужно прописать строку подключения, например: Provider=MSDASQL.1;PersistSecurityInfo=False;DataSource=Файлы dBASE Эту строку можно ввести программно, или создать подключение при проектировании (я так и сделал). Поставщик данных в примере оставлен по умолчанию MicrosoftOLEDBProviderforODBCDrivers, а в качестве источника данных (вкладка "Подключение" редактора связей TADOConnection ) используются файлы dBase. Не следует забывать и про свойство LoginPrompt, которое следует переводить в False, чтобы программа не запрашивала имя пользователя и пароль при каждом подключении. А также нужно сразу открыть TADOConnection, установив его свойство Connected в True. В свойстве Connection компонента TADOQuery следует выбрать ADOConnection1. Пример реализован, как событие нажатия на кнопку: procedure TfMain.Button1Click(Sender: TObject); var s: String; begin {Создаем текст запроса} s := 'CREATE TABLE MyTab(Key1 INT, Name CHAR(20), '+ ' MyFloat FLOAT, MyDate DATE)'; {Создаем таблицу} ADOQuery1.SQL.Clear; ADOQuery1.SQL.Add(s); ADOQuery1.ExecSQL; {Открываем таблицу} ADOQuery1.SQL.Clear; ADOQuery1.SQL.Add('SELECT * FROM MyTab'); ADOQuery1.Open; Как видите, создается четыре поля - целый тип, строковый размером 20 символов, вещественный и тип Дата. Последний тип не входит в стандартное описание ANSI -типов, тем не менее, работает в большинстве СУБД. Можете также поэкспериментировать и с типом BOOLEAN (Логический). Итак, в переменную s мы вносим строку записи SQL -запроса. Затем очищаем свойство SQL, на случай, если там уже имелся запрос. Далее этот запрос мы заносим в свойство SQL, и методом ExecSQL выполняем его. С открытием таблицы мы уже неоднократно сталкивались. В результате выполнения кода создается и открывается файл MyTab.dbf, который находится в той же папке, что и приложение. Лекция 10.Сохранение древовидных структур в базе данных Цель: Рассмотрение вопрса по сохранению древовидных структур в базе данных, как можно сохранить дерево с неограниченным количеством ветвей в таблицу, и как считать его обратно в компонент TreeView. Древовидные структуры не относятся напрямую к программированию баз данных, тем не менее, программисту нередко приходится "изобретать велосипед", придумывая различные решения сохранения таких структур в таблице, и обратное их считывание в дерево. Типичный пример дерева - всем знакомое дерево каталогов. Примеров таких структур множество - это могут быть отделы в каком-либо учреждении или разделы библиотеки. Посмотрим на рисунок с фрагментом дерева разделов библиотеки: Рис. 10.1. Дерево разделов Основная сложность хранения деревьев в таблице - это то, что мы не знаем заранее, какова будет глубина вложенности разделов. Можно было бы создать таблицу с 10 полями, например. Но если вложенных разделов будет меньше, то таблица будет неэффективна - останется много пустых полей. А если больше - ограничивать пользователя? Самый простой способ сохранения структуры дерева и ее считывания обратно воспользоваться тем, что дерево - это список узлов, и имеет хорошо знакомые нам методы: //сохраняем в файл: TreeView1.SaveToFile('myfile.txt'); //читаем из файла: TreeView1.LoadFromFile('myfile.txt'); Однако этот способ имеет массу недостатков. Во-первых, в результате получим простой текстовый файл, в котором вложенные узлы располагаются ниже родителя и имеют отступ. Пользователь легко может случайно или намеренно испортить такой файл, отредактировав или просто удалив его с диска, и программа будет работать с ошибками. Во-вторых, обычно древовидная структура тесно связана с другими данными, например, таблица отделов предприятия связана со служащими этого предприятия - запись каждого служащего имеет ссылку на отдел, где он работает. Если структуру предприятия хранить в простом текстовом файле, то такую связь сложно будет обеспечить. Когда программист впервые сталкивается с необходимостью хранения древовидных структур в базе данных, обычно он первым делом подключается к Интернету и ищет какой-нибудь компонент, который бы позволил это делать. Но не все нестандартные компоненты работают качественно, да и зачем искать какой-то новый компонент, когда имеется стандартный TreeView на вкладке Win32 Палитры компонентов? Именно с этим компонентом мы и будем работать в данной лекции. Рецептов работы с деревьями в базах данных много, мы рассмотрим лишь один из них, достаточно эффективный и в то же время простой. Смысл этого способа состоит в том, чтобы в каждой записи таблицы сохранять номер узла раздела, номер его родителя, если он есть, и название узла. В случае если узел не имеет родителя (главный узел, например, "Художественная литература" в рисунке 10.1), то в соответствующее поле запишем ноль. Подготовка проекта Для реализации примера нам потребуется новая база данных. Загрузите MSAccess и создайте базу данных " TreeBD ", а в ней таблицу " Razdels ". Вообще-то, в базе данных MSAccess как таблицы, так и поля могут иметь русские названия, однако мы будем использовать средства SQL, который не всегда корректно обрабатывает русские идентификаторы. Кроме того, данный способ можно использовать в любой СУБД, а далеко не все из них так предупредительны, как MSAccess, поэтому название таблицы и ее полей выполним латиницей. Таблица будет иметь три поля: Таблица 10.1 . Поля таблицы "Разделы" № Имя поля Тип поля Дополнение 1 R_Num Счетчик Ключевое поле 2 R_ Parent Числовой Целое 3 R_Name Текстовый Длина 50 символов Созданную базу данных сохраните в папке, где будем разрабатывать наш проект (не забудьте сделать резервную копию пустой базы данных на всякий случай.). Далее создадим в Delphi новый проект и простую форму: Рис. 10.2 . Форма для работы с деревом Как всегда, назовите форму fMain, в свойстве Caption напишите "Реализация сохранения дерева в БД", модуль формы сохраните как Main, а проект в целом назовите, например, TreeToBD. Сделанная база данных TreeBD должна быть в той же папке, что и проект. Далее установите компонент TreeView (дерево) с вкладки Win32. Его свойству Align присвойте alLeft, чтобы дерево заняло весь левый край. Затем можете установить сплиттер - разделитель, ухватившись за который пользователь сможет менять ширину дерева. Компонент Splitter находится на вкладке Additional и его свойство Align по умолчанию равно alLeft - разделитель "прилепится" к правому краю дерева. Правее установите сетку DBGrid с вкладки DataControls, и его свойству Align присвойте alClient, чтобы сетка заняла все оставшееся место. Ни главное меню, ни панель инструментов нам здесь не потребуются, используем лишь два всплывающих PopupMenu - первый для дерева, второй для сетки (выберите соответствующие PopupMenu в свойстве PopupMenu этих компонентов). Далее с вкладки ADO нам потребуется компонент ADOConnection для соединения с базой данных, таблица ADOTable и запрос ADOQuery для вспомогательных нужд. С вкладки DataAccess - компонент DataSource, для связи сетки с таблицей. Подключите ADOConnection к базе данных и откройте соединение ( "ADO.Связь с таблицей MSAccess" ). Таблицу подключите к ADOConnection(свойство Connection ), затем выберите в свойстве TableName нашу таблицу " Razdels ", а свойство Name переименуйте в tRazdels - так будем обращаться к таблице. Для удобства отображения названия полей откройте редактор полей таблицы (дважды щелкнув по ней), добавьте все поля и у каждого поля измените свойство DisplayLabel, соответственно, на "№", "Родитель" и "Название". Не забудьте открыть таблицу. Компонент DataSource подключите к tRazdels, а сетку - к DataSource, в сетке должны отобразиться поля. Кроме того, переименуйте свойство Name запроса ADOQuery1 в Q1, ведь нам часто придется обращаться к нему по имени. Запрос также подключите к ADOConnection, но делать его активным не нужно. На этом приготовления закончены. Создание и сохранение в таблицу дерева разделов Работа с деревьями состоит из двух этапов: 1. Сохранение дерева в таблицу. 2. Считывание дерева из таблицы. В этом разделе лекции разберем первый этап. Щелкните дважды по компоненту PopupMenu1, который "привязан" к дереву, и создайте в нем следующие разделы: Создать главный раздел Добавить подраздел к выделенному Переименовать выделенный Удалить выделенный Свернуть дерево Развернуть дерево Все эти команды относятся к работе с разделами дерева. Прежде всего, создадим обработчик для команды "Создать главный раздел". Листинг процедуры смотрите ниже: {Создать главный раздел} procedureTfMain.N1Click(Sender: TObject); var s: String; //для получения имени раздела (подраздела) NewRazd: TTreeNode; //для создания нового узла дерева begin //вначале очистим s s:= ''; //Получим в s имя нового раздела: ifnotInputQuery('Ввод имени раздела', 'Введите заголовок раздела:', s) thenExit; //снимаем возможное выделение у дерева: TreeView1.Selected:= nil; //создаем главный раздел (ветвь): NewRazd:= TreeView1.Items.Add(TreeView1.Selected, s); //Сразу же сохраняем его в базу: tRazdels.Append; //добавляем запись tRazdels['R_Parent']:= 0; //не имеет родителя //присваиваем значение созданного раздела: tRazdels['R_Name']:= NewRazd.Text; //сохраняем изменения в базе: tRazdels.Post; end; Разберем код. Переменная NewRazd имеет тип TTreeNode, к которому относятся все разделы и подразделы (узлы) дерева. В текстовую переменную s с помощью функции InputQuery() мы получаем имя нового главного узла. Функция имеет три строковых параметра: 1. Заголовок окна. 2. Пояснительная строка. 3. Переменная, куда будет записан введенный пользователем текст. Если переменная, передаваемая в качестве третьего параметра, пуста, то поле ввода будет пустым. Если же в ней содержался текст - он будет выведен как текст "по умолчанию". Функция возвращает True, если пользователь ввел (или изменил) текст, и False в противном случае. В результате работы функции для пользователя будет выведено простое окно с запросом: Рис. 10.3 . Окно функции InputQuery() Далее строкой TreeView1.Selected:= nil; мы снимаем выделение, если какой либо раздел был выделен, ведь мы создаем главный раздел, не имеющий родителя. Свойство Selected компонента TreeView указывает на выделенный узел и позволяет производить с ним различные действия, например, получить текст узла: TreeView1.Selected.Text; А присваиваемое значение nil (ничто) снимает всякое выделение, если таковое было. Далее мы создаем сам узел: NewRazd:= TreeView1.Items.Add(TreeView1.Selected, s); Разберем эту строку подробней. Переменная NewRazd - это новый узел дерева. Каждый узел - объект, обладающий своими свойствами и методами. Все узлы хранятся в списке - свойстве Items дерева TreeView, а метод Add() этого свойства позволяет добавить новый узел. У метода два параметра - выделенный узел (у нас он равен nil ) и строка текста, которая будет присвоена новому узлу. Таким образом, в дереве появляется новый главный узел. Затем мы сохраняем его в базу данных, предварительно добавив в таблицу новую запись: tRazdels.Append; //добавляем запись tRazdels['R_Parent']:= 0; //не имеет родителя //присваиваем значение созданного раздела: tRazdels['R_Name']:= NewRazd.Text; //сохраняем изменения в базе: tRazdels.Post; Вы помните, что такие методы, как Append или Insert автоматически переводят таблицу в режим редактирования, поэтому вызывать метод Edit излишне? Обратите внимание на то, что мы сохраняем ноль в поле "R_ Parent ", так как это главный раздел, не имеющий родителя. Свойство Text нового узла NewRazd содержит название нового узла, которое мы присваиваем полю "R_Name". Далее сгенерируем процедуру для команды меню "Добавить подраздел к выделенному": {Добавить подраздел к выделенному разделу(подразделу)} procedure TfMain.N2Click(Sender: TObject); var s: String; //для получения имени раздела (подраздела) z: String; //для формирования заголовка окна NewRazd: TTreeNode; //для создания нового узла дерева begin //Проверим - есть ли выделенный раздел? //Если нет - выходим: if TreeView1.Selected = nil then Exit; //вначале очистим s s:= ''; //сформируем заголовок окна запроса: z:= 'Раздел " + TreeView1.Selected.Text + '"; //Получим в s имя нового раздела: ifnotInputQuery(PChar(z), 'Введите заголовок подраздела:', s) thenExit; //создаем подраздел: NewRazd:= TreeView1.Items.AddChild(TreeView1.Selected, s); //перед сохранением подраздела в базу, прежде получим //номер его родителя: Q1.SQL.Clear; Q1.SQL.Add('select * from Razdels where R_Name="+ NewRazd.Parent.Text+"); Q1.Open; //Теперь сохраняем его в базу: tRazdels.Append; //добавляем запись //присваиваем № родителя: tRazdels['R_Parent']:= Q1['R_Num']; //присваиваем название узла: tRazdels['R_Name']:= NewRazd.Text; //сохраняем изменения в базе: tRazdels.Post; end; Код этой процедуры очень похож на код предыдущей, но есть и отличия. Прежде всего, мы проверяем - а имеется ли выделенный раздел? Ведь фокус ввода мог быть и на сетке DBGrid, когда пользователь щелкнул правой кнопкой по дереву, и выбрал эту команду. В этом случае, если не делать проверки, мы получим ошибку, пытаясь добавить дочерний узел к пустоте. Далее, мы ввели строковую переменную z, чтобы сформировать запрос. Ведь пользователю будет удобней, если в окне InputQuery() он сразу увидит, к какому именно разделу он добавляет подраздел. Затем, при добавлении дочернего узла вместо метода Add() мы используем метод AddChild(). Ну и, наконец, при сохранении узла в таблицу мы записываем не только созданный узел, но и номер его родителя, получив его с помощью запроса Q1.SQL.Add('select * from Razdels where R_Name='"'+ NewRazd.Parent.Text+'"'); Запрос формирует набор данных с единственной строкой - записью родителя добавляемого элемента. Поле Q1['R_Num'], как вы понимаете, хранит номер этого родителя в запросе. Код процедуры переименования выделенного раздела выглядит так: {Переименовать выделенный раздел (подраздел)} procedureTfMain.N3Click(Sender: TObject); var s: String; //для получения имени раздела (подраздела) z: String; //для формирования заголовка окна begin //Проверим - есть ли выделенный раздел? //Если нет - выходим: if TreeView1.Selected = nil then Exit; //получаемтекущийтекст: s:= TreeView1.Selected.Text; //формируем заголовок: z:= 'Редактирование "' + s + '"'; //если не изменили, выходим: ifnotInputQuery(PChar(z), 'Введите новый заголовок:', s) thenExit; //находим эту запись в таблице, учитывая, что ее по каким то //причинам может и не быть: if not tRazdels.Locate('R_Name', TreeView1.Selected.Text, []) thenbegin ShowMessage('Ошибка! Указанный раздел не существует в таблице.'); Exit; end; //if //если до сих пор не вышли из процедуры, значит запись найдена, //и является текущей.изменяем ее: tRazdels.Edit; tRazdels['R_Name']:= s; tRazdels.Post; //теперь меняем текст выделенного узла: TreeView1.Selected.Text := s; end; Здесь комментарии достаточно подробны, чтобы вы разобрались с кодом. Следует обратить внимание на то, что вначале мы исправляем запись в таблице, и только потом - в узле. Если бы мы сначала исправили текст узла, как бы затем нашли старую запись в таблице? Пришлось бы вводить дополнительную переменную для хранения старого текста. Удаляется выделенный узел еще проще: {Удалить выделенный раздел (подраздел)} procedureTfMain.N4Click(Sender: TObject); var s: String; //для строки запроса begin //Проверим - есть ли выделенный раздел? //Если нет - выходим: if TreeView1.Selected = nil then Exit; //иначе формируем строку запроса: s:= 'Удалить "' + TreeView1.Selected.Text + '"?'; //запросим подтверждение у пользователя: ifApplication.MessageBox(PChar(s), 'Внимание!', MB_YESNOCANCEL+MB_ICONQUESTION) <> IDYES then Exit; //если не вышли - пользователь желает удалить раздел. //найдем и удалим его вначале из таблицы: if tRazdels.Locate('R_Name', TreeView1.Selected.Text, []) then tRazdels.Delete; //теперь удаляем раздел из дерева: TreeView1.Items.Delete(TreeView1.Selected); end; Далее нам осталось сгенерировать процедуры для сворачивания и разворачивания дерева. Делается это одной строкой: {свернуть дерево} TreeView1.FullCollapse; {развернуть дерево} TreeView1.FullExpand; Итак, метод FullCollapse дерева TreeView сворачивает его узлы, а метод FullExpand разворачивает. Теперь сохраните проект и скомпилируйте его. Попробуйте заполнить дерево разделами и подразделами, убедитесь, что параллельно данные сохраняются и в таблице. Чтение древовидной структуры из таблицы Прежде всего, создадим пункты для второго всплывающего меню, которое "привязано" к сетке DBGrid. Пункты будут такими: Очистить дерево Заполнить дерево Для очищения дерева нам требуется просто очистить его свойство Items, делается это одной строкой: TreeView1.Items.Clear; Займемся заполнением дерева. Прежде разберемся с алгоритмом. Вначале нам потребуется считать из таблицы в дерево все узлы, не имеющие родителя (главные). Затем мы сделаем запрос, в котором получим пару "Родительский узел - Дочерний узел" всех подразделов. То есть, главные узлы будут отфильтрованы этим запросом. После чего нам останется пройти от первой до последней записи этого набора данных, добавляя дочерний узел к его родителю. Создайте обработчик команды "Заполнить дерево". Код обработчика будет таким: {Заполнить дерево} procedure TfMain.N10Click(Sender: TObject); begin //если таблица пуста, сразу выходим: if tRazdels.IsEmpty then Exit; //если в старом дереве есть узлы, очистим их: TreeView1.Items.Clear; //вначале запросим все главные узлы: Q1.SQL.Clear; Q1.SQL.Add('select * from Razdels where R_Parent=0'); Q1.Open; if Q1.IsEmpty then Exit; //еслиНДпуст, выходим. //теперь занесем их в дерево: while not Q1.Eof do begin TreeView1.Selected := nil; TreeView1.Items.Add(TreeView1.Selected, Q1.FieldByName('R_Name').AsString); Q1.Next; end; //while //делаем запрос, выводящий пару: Родительский узел - Дочерний узел //и поочередно прописываем их в дерево процедурой TreeViewAddChild: Q1.SQL.Clear; Q1.SQL.Append('select r.R_Name, d.R_Name '+ 'from Razdels r, Razdels d '+ 'where r.R_Num=d.R_Parent'); Q1.Open; ifQ1.IsEmptythenExit; //если нет вложенных узлов, выходим Q1.First; while not Q1.Eof do begin TreeViewAddChild(Q1.Fields[0].AsString, Q1.Fields[1].AsString); Q1.Next; end; //while //распахиваем дерево: TreeView1.FullExpand; end; Разберем этот код. В самом начале мы проверяем - не пуста ли таблица, и если это так, то выходим из процедуры, ничего не делая. Затем мы очищаем старые данные из дерева. Конечно, у нас предусмотрена очистка дерева во втором всплывающем меню, но ведь пользователь может вызвать команду "Заполнить дерево" дважды, и тогда у нас могут возникнуть проблемы. Далее мы создаем запрос: //вначале запросим все главные узлы: Q1.SQL.Clear; Q1.SQL.Add('select * from Razdels where R_Parent=0'); Q1.Open; if Q1.IsEmpty then Exit; //еслиНДпуст, выходим. Здесь после выполнения метода Open мы получаем все разделы, не имеющие родителя. Иначе говоря, главные ветви дерева. Потом мы проверяем - а есть ли главные узлы в таблице? Ведь таблица может быть пуста или испорчена, и тогда дальнейшее выполнение программы не имеет смысла. Если таблица не пуста и главные разделы в ней есть, то мы обходим полученный запросом набор данных от первой до последней записи, сразу же добавляя эти главные узлы в дерево: while not Q1.Eof do begin TreeView1.Selected := nil; TreeView1.Items.Add(TreeView1.Selected, Q1.FieldByName('R_Name').AsString); Q1.Next; end; //while В результате, в наше дерево пропишутся все главные разделы. После этого нам нужно будет сделать еще один запрос, который выведет все записи, имеющие родителя, в виде "Раздел - подраздел". Запрос формируется следующим образом: Q1.SQL.Clear; Q1.SQL.Append('selectr.R_Name, d.R_Name '+ 'from Razdels r, Razdels d '+ 'where r.R_Num=d.R_Parent'); Q1.Open; ifQ1.IsEmptythenExit; //если нет вложенных узлов, выходим Обратите внимание, в запросе мы используем две копии одной и той же таблицы! Подробнее о псевдонимах таблиц в запросах смотрите "Краткий курс языка запросов SQL" . В результате этого запроса мы получим примерно такой набор данных: Рис. 10.4 . Полученный набор данных Далее мы обрабатываем полученный НД от первой до последней записи: Q1.First; while not Q1.Eof do begin TreeViewAddChild(Q1.Fields[0].AsString, Q1.Fields[1].AsString); Q1.Next; end; //while Здесь мы использовали обращение к полю не по имени, а по индексу, то есть, Q1.Fields[0] - это первое поле. Как видно из рисунка, дважды обращаясь в запросе к одному и тому же полю, мы получим разные названия этих полей (R_Name и R_Name1). Поэтому обращаться к полю по его имени не получится. В цикле мы двигаемся от первой записи к последней, вызывая процедуру TreeViewAddChild, которой у нас еще нет. И в конце процедуры мы распахиваем все узлы полученного дерева. Теперь сделаем процедуру, которой будем передавать все полученные подразделы. В начале модуля, в разделе private, объявите следующую процедуру: private { Privatedeclarations } procedure TreeViewAddChild(rod, doch: String); Здесь, в параметре rod мы будем передавать название родительского раздела, а в doch - название подраздела. Не убирая курсор с названия процедуры, нажмите <Ctrl + Shift + C>. Эта комбинация клавиш автоматически генерирует тело объявленной процедуры. Код процедуры следующий: procedure TfMain.TreeViewAddChild(rod, doch: String); vari : Integer; //счетчик begin //ищем родительский узел в дереве и выделяем его: for i := 0 to TreeView1.Items.Count-1 do begin //если родитель найден, выделяем его и прерываем цикл: if TreeView1.Items[i].Text = rod then begin TreeView1.Items[i].Selected := True; Break; end; //if end; //for //теперь родитель имеет выделение и мы можем добавить к нему //наш узел: TreeView1.Items.AddChild(TreeView1.Selected, doch); end; Здесь мы вначале циклом for обходим дерево, ища родительский узел. Если узел найден, мы выделяем его в дереве и прерываем цикл. Теперь к выделенному родительскому узлу мы добавляем подраздел: TreeView1.Items.AddChild(TreeView1.Selected, doch); Вот и все. Сохраните проект, скомпилируйте и попробуйте программу в работе. Все должно работать безошибочно. Единственное замечание: вам придется следить за правильностью данных в базе (реализовать бизнес-правила). Если пользователь удалит какой-нибудь родительский узел, нужно будет удалить и все его вложенные узлы. Реализовать это несложно, сделайте такие правила самостоятельно. Данный прием работы с древовидными структурами можно использовать в любой СУБД для самых разных целей. Лекция 11. Отчеты. QuickReport Цель: Создание профессиональных отчетов, как простых, так и связанных. Изучить назначение основных компонентов из набора QuickReport. Отчеты - это один из основных результатов работы проекта с базами данных. Для чего и создаются базы данных, как не для получения отчетов? Отчет подразумевает, что программа выбирает необходимые данные из НД ( TTable, TQuery и т.п.) и выводит их на экран в удобном виде. Сам отчет можно не только просматривать, но и выводить его на печать. Лист профессионального отчета представляет собой сгенерированное графическое изображение, картинку, другими словами, данные в отчете редактировать уже нельзя. Не получится также выделить и скопировать в буфер обмена текст отчета. Готовый отчет выводит информацию, разбитую на страницы, и подготовленную к печати. Отчеты создаются специальными наборами компонентов. Имеется очень много отчетов сторонних разработчиков, как платных, так и бесплатных, которые можно найти в Internet. В этой лекции мы разберем стандартный набор компонентов QuickReport, который поставляется вместе с Delphi. Установка QuickReport. QuickReport представляет собой стандартный набор компонентов для создания отчетов. Он поставляется вместе с Delphi, но не устанавливается в палитру компонентов автоматически. Нам придется установить его самостоятельно. Если пакет QuickReport у вас еще не установлен (на палитре компонентов отсутствует вкладка QReport ), то загрузите Delphi и закройте все открытые проекты (File >CloseAll). Выбрерите пункт меню "Component -> Install Packages". Нажмите кнопку "Add" и выберите пакет "dclqrt70.bpl", который по умолчанию устанавливается по адресу: c:\Program Files\Borland\Delphi7\bin\dclqrt70.bpl и нажмите кнопку "Открыть". Далее, нажмите кнопку "ОК" - пакет компонентов QuickReport установится, и его вкладка будет самой последней на Палитре компонентов. При желании можно перетащить ее мышью на другое место, поближе к началу. Простой отчет Создадим простой отчет на основе программы для отдела кадров из предыдущих лекций. Чтобы не менять старый проект, скопируйте его целиком в новую папку и откройте. Кнопок на панели инструментов у нас здесь достаточно, для работы с отчетами создадим главное меню. Добавьте компонент MainMenu и создайте разделы: Таблица 11.1. Разделы главного меню Р Подразделы аздел Ф Выход айл О Кадры По телефонам По тчеты адресам Для самого отчета нам потребуется новая форма. Создайте ее, свойству Name присвойте значение fRepKadr, а модуль сохраните под именем RepKadr. Сразу же командой "File ->UseUnit" подключим к этой форме модуль данных DM, а к главной форме - только что созданный новый модуль. В палитре компонентов перейдем на вкладку QReport. Самым первым компонентом на вкладке является QuickRep - основа всех отчетов. Установите его на новую форму, и он примет вид разлинованного листа. Это своего рода холст, на котором мы будем собирать различные части нашего отчета: Рис. 11.1. Пустой "холст" QuickRep Выделите QuickRep и обратите внимание на его свойства. В самом верху находится свойство Bands (Ленты, полосы - англ.). Это раскрывающееся свойство, оно содержит шесть параметров. Щелкните по плюсу слева от свойства, чтобы раскрыть его. По умолчанию, все параметры имеют значение False, то есть, не установлены. Если какойлибо параметр перевести в значение True, на холсте появится соответствующая полоса. Попробуйте установить все параметры. Разберемся с их назначением. HasColumnHeader - Заголовки колонок. Здесь мы будем вписывать названия колонок таблицы. HasDetail - Детальная информация. На этой ленте формируются строки таблицы. HasPageFooter - Подвал (нижний колонтитул). Здесь можно установить информацию, которая будет появляться в нижней части каждой страницы. HasPageHeader - Шапка (верхний колонтитул). Здесь можно установить информацию, которая будет появляться в верхней части каждой страницы. HasSummary - Суммарная информация. Содержимое этой полосы печатается один раз в самом конце отчета. HasTitle - Заголовок отчета. Переведите в True полосы HasPageHeader, HasTitle, HasColumnHeader, HasDetail и HasPageFooter. Не установленной останется только полоса HasSummary. Если вы дважды щелкните мышью по свободному месту холста, появится настроечное окно: Рис. 11.2. Окно настроек компонента QuickRep В этом окне можно выполнить большинство настроек, причем в Инспекторе объектов соответствующие свойства будут изменены автоматически. Как видите, установленные нами полосы отмечены "галочкой" в разделе Bands окна. Выше располагается раздел Pageframe, в котором можно задать обрамление для верхней ( Top ), нижней ( Bottom ), левой ( Left ) и правой ( Right ) границ холста, а также изменить цвет и ширину обрамления. Те же действия можно выполнить в Инспекторе объектов с помощью параметров раскрывающегося свойства Frame (пока открыто окно настроек, менять свойства в Инспекторе Объектов не получится). Еще выше располагается раздел Other, где можно установить общие данные для холста - шрифт, размер шрифта и единицы измерения (по умолчанию mm - миллиметры). В Инспекторе объектов за это отвечают свойства Font и Units. Далее находится раздел Margins (Границы, края), где можно задать расстояния от краев листа до рабочей части холста. На самом верху окна располагается раздел Papersize (Размер бумаги), где задаются тип листа и его размеры. Данные этих двух разделов можно изменить в Инспекторе объектов в раскрывающем свойстве Page. Еще следует обратить внимание на свойство Options, которое имеет три параметра: FirstPageHeader - Разрешает печать заголовков (шапку) первой страницы, если равно True. LastPageFooter - Разрешает печать подвала последней страницы, если равно True. Compression - Разрешает сжатие отчета при формировании из него метафайла (отчет представляет собой изображение), если равно True. Свойство PrintIfEmpty разрешает ( True ) или запрещает ( False ) печатать отчет, если в нем нет никаких данных. Свойство ShowProgress разрешает или запрещает показывать индикатор процесса печати отчета. По умолчанию индикатор разрешен. Свойство SnapToGrid разрешает или запрещает привязывание компонентов к сетке. По умолчанию привязка разрешена. Свойство Zoom имеет тип Integer и позволяет изменить масштаб отображения отчета при его разработке. Значение 100 указывает, что отчет показывается в 100% от листа бумаги. Изменение этого свойства не влияет на масштаб печати отчета или его предварительного просмотра. Теперь приступим к формированию отчета. На холсте у нас уже должны быть расположены пять полос. Теперь мы можем на эти полосы устанавливать другие компоненты. На самом верху холста находится полоса PageHeader, которая, как мы уже знаем, является верхним колонтитулом. Установите в левой части этой полосы компонент QRSysData - компонент с различного рода системной информацией. Нас интересует свойство Data этого компонента. Data содержит несколько свойств, формирующих отображаемую информацию.Разберем эти свойства. qrsDate (Вывод текущей даты) qrsDateTime (Вывод текущих даты и времени) qrsDetailCount (Количество строк формируемой таблицы) qrsDetailNo (Номер текущей строки таблицы) qrsPageNumber (Номер текущей страницы отчета) qrsReportTitle (Заголовок отчета) qrsTime (Вывод текущего времени) Установим для этого свойства значение qrsDateTime, чтобы пользователь мог видеть, когда был сформирован отчет. Далее выделим всю полосу PageHeader и в свойстве Frame переведем в True параметр DrawBottom. Это свойство позволяет задать обрамление выделенной полосе, а параметр DrawBottom рисует линию в нижней части полосы. То есть, мы визуально отделили колонтитул от данных листа. В нижней части холста располагается полоса PageFooter (подвал). Здесь желательно установить верхнюю линию в свойстве Frame, отделив от данных нижний колонтитул. А по центру полосы установить еще один компонент QRSysData, установив свойство Data в значение qrsPageNumber. Этот компонент будет выводить номер текущей страницы в нижней части листа. Примечание: для того, чтобы увидеть отчет в окне предварительного просмотра, не обязательно компилировать программу. Достаточно щелкнуть правой кнопкой мыши по свободному месту листа QuickRep, и в контекстном меню выбрать команду Preview. При этом следует иметь в виду, что такие данные, как значения вычисляемых полей, например, видны не будут. Эти данные станут доступны только в режиме выполнения программы. Колонтитулы мы установили, займемся данными. Прежде всего, напишем заголовок отчета. Для этого установите компонент QRLabel в центре полосы Title. QRLabel похож на обычный Label и служит тем же целям: выводит на лист какой то текст. Выделите его, и в свойстве Caption напишите "Отчет по кадрам". Чтобы заголовок был красивым, щелкните дважды по свойству Font, чтобы открылось окно шрифта. Здесь установите шрифт TimesNewRoman, начертание выберите жирное, а размер шрифта пусть будет 18 (вы можете использовать настройки по собственному выбору). Можно изменять и цвет шрифта, но при этом имейте в виду, что чаще всего отчеты печатают на чернобелых принтерах, так что злоупотреблять разными цветами не рекомендуется. Далее займемся полосой ColumnHeader (Заголовки колонок). Здесь установите рядом пять компонентов QRLabel, в свойстве Caption которых напишите Фамилия Имя Отчество Дата рождения Образование Это будут названия колонок таблицы. Шрифт этих компонентов также желательно сделать крупнее, но не больше заголовка. Далее займемся полосой Detail, на которой, собственно, и будет формироваться таблица. Здесь нам нужно будет в самом крайнем положении слева установить компонент QRSysData, в свойстве Data которого выбрать qrsDetailNo - перед каждой строкой будет выходить ее номер. Далее установите пять компонентов QRDBText, в которых будут отражаться данные из соответствующих полей таблицы. Эти компоненты соответствуют обычномуDBText, с которым мы неоднократно сталкивались. Расположите их точно под названиями столбцов, чтобы таблица была красивой. При этом может оказаться, что компонент QRSysData "наплывает" на QRDBText - ничего страшного, данные все равно не будут мешать друг другу. Выделите все QRDBText, и в их свойстве DataSet выберите нашу таблицу fDM.TLichData, затем поочередно в свойстве DataField этих компонентов выберите соответствующие поля таблицы: Фамилия Имя Отчество Дата_Рожд Образование Кроме того, сам компонент QuickRep1, который является "холстом" отчета, также должен знать, из какой таблицы ему нужно брать данные. Поэтому выделите его, и в свойстве DataSet также выберите нашу таблицу fDM.TLichData. Если этого не сделать, то в отчете будет выходить лишь текущая запись таблицы, а не все ее записи. Собственно, отчет уже готов: Рис. 11.3. Отчет Вернитесь в главное окно проекта и сгенерируйте обработку команды меню "Отчеты - Кадры". В созданной процедуре напишите такую строку (вы ведь добавили к главному окну командой File - UseUnit наш отчет?): {Отчет Кадры} procedure TfMain.N5Click(Sender: TObject); begin fRepKadr.QuickRep1.PreviewModal; end; После того, как вы сохраните проект, скомпилируете его и выполните команду меню, появится подобное окно с отчетом: Рис. 11.4. Окно отчета Как видите, окно уже содержит весь необходимый инструментарий в панели инструментов - позволяет настроить вид отчета, листать его страницы, сделать установку принтера и распечатать отчет, а также сохранить отчет в специальный файл с расширением *.qrp с возможностью последующей его загрузки. К сожалению, интерфейс этого окна доступен только на английском языке. Наш отчет был бы красивей, если бы полученная таблица была очерчена рамкой. Исправим этот недостаток. На вкладке QReport имеется компонент QRShape, который позволяет рисовать простейшие линии и фигуры. Он имеет свойство Shape, в котором можно задать нужную фигуру. Возможные значения этого свойства: qrsCircle (Круг) qrsHorLine (Горизонтальная линия) qrsRectangle (Прямоугольник) qrsRightAndLeft (Прямоугольник с очерченными левым и правым краями) qrsTopAndBottom (Прямоугольник с очерченными верхним и нижним краями) qrsVertLine (Вертикальная линия) Этот компонент можно использовать по-разному. Например, можно установить по одной горизонтальной линии сверху и снизу полосы Detail, а затем вертикальными линиями отделить каждый столбце страницы. Я сделал проще: каждый столбец заключил в прямоугольник (шесть компонентов QRShape ), а чтобы QRShape не перекрывал текст, щелкнул по ним правой кнопкой и выбрал команду Control ->SendtoBack (поместить на задний план): Рис. 11.5. Компоненты QRShape играют роль границ таблицы В результате таблица отчета приняла вид: Рис. 11.6. Таблица отчета с границами Результат может не сразу получиться таким - придется поэкспериментировать с расположением и размерами прямоугольников QRShape. Отчет из связанных таблиц Часто бывает, когда недостаточно получить отчет по данным только из одной таблицы. И здесь можно поступить двумя способами: 1. Воспользоваться набором данных ADOQuery и с помощью SQL запроса получить нужные данные из двух таблиц. 2. Воспользоваться компонентом QRSubDetail, который специально предназначен для получения данных из связанной таблицы. Этим компонентом мы и воспользуемся. Итак, QRSubDetail - это полоса, которая импортирует в отчет данные из подчиненной таблицы. Создайте в проекте новую форму, назовите ее fRepTelephons, а модуль сохраните как RepTelephons. Сразу же к этому окну командой File ->UseUnit подключите модуль с наборами данных DM, а к главному модулю подключите только что созданный RepTelephons. Установите на новую форму основу отчета QuickRep. Проверьте, чтобы он не был смещен по отношению к листу (свойства Left и Top равны 0). В свойстве DataSet компонента-основы выберите fDM.TLichData, то есть, таблицу с личными данными. Теперь создайте на основе полосы PageHeader, Title, ColumnHeader и Detail. На верхнюю полосу PageHeader установите по краям два компонента QRSysData, в свойстве Data у первого выберите qrsDateTime, а у второго qrsPageNumber. Кроме того, переведите в True параметр DrawBottom у свойства Frame полосы PageHeader, чтобы отделить линией верхний колонтитул. Далее, на полосу Title установите один компонент QRLabel, на котором напишите заголовок "Отчет по телефонам сотрудников". Измените шрифт, начертание и размер, как в прошлом примере, и отцентрируйте заголовок по полосе. Ниже идет полоса ColumnHeader с заголовками таблицы. Как и в прошлом примере, требуется из компонентов QRLabel сформировать заголовки столбцов, но в этот раз ограничимся только тремя заголовками: "Фамилия", "Имя" и "Отчество". К слову сказать, чтобы не делать работу дважды, вы можете открыть форму fRepKadr из прошлого примера, выделить нужные компоненты и командой всплывающего меню Edit ->Copy скопировать их. Затем перейти в новую форму, выделить нужную полосу, и командой Edit ->Paste вставить эти компоненты. Далее у нас идет полоса Detail, на которой нам нужно разместить три компонента QRDBText, которые привязать к соответствующим полям (не забудьте про свойства DataSet и DataField этих компонентов). Еще в свойстве Frame полосы Detail желательно перевести параметр DrawTop в True, чтобы каждая запись отчета отделялась линией. Пока что все, что мы делали, было практически таким же, как в прошлом отчете. Теперь добавим в отчет связанные данные из другой таблицы. Установите компонент QRSubDetail - эта полоса должна быть самой нижней. Сначала нужно выбрать главный по отношению к этому компонент: в свойстве Master выберите QuickRep1. Кроме того, полоса QRSubDetail должна знать, откуда листать данные, поэтому в свойстве DataSet полосы выберите fDM.TTelephones. Далее установим на полосу один компонент QRLabel, напишем на нем "Телефон:". Слева от него установите два компонента QRDBText, в свойстве Dataset которых выберите fDM.TTelephones, а в свойстве DataField выберите соответственно, поля "Телефон" и "Примечание". Теперь мышью немного перетащите нижний край полосы, чтобы сделать ее поуже. В результате у вас должна получиться форма, подобная этой: Рис. 11.7. Отчет по телефонам Наш отчет готов. Создайте процедуру вызова этого окна в команде главного меню Отчеты -> По телефонам, и пропишите там вызов fRepTelephons.QuickRep1.PreviewModal; Сохраните проект, скомпилируйте и запустите его. В результате выбора этой команды мы получим подробный отчет по телефонам сотрудников: Рис. 11.8. Окно отчета по телефонам Отчет по адресам сотрудников создается точно таким же образом, и теперь вы сможете сделать его самостоятельно. Экспорт отчета в другие форматы Отчет можно не только распечатать. Его также можно сохранить в специальном формате *.qrp, а затем загрузить в окно предварительного просмотра. Для этого соответственно служат кнопки " SaveReport " и " LoadReport " на панели инструментов окна предварительного просмотра: Рис. 11.9.Кнопки "Save Report" и "Load Report" Однако бывают случаи, когда отчет желательно сохранить в каком-нибудь общем формате, например, в текстовом, или html( web -страница). Тогда отчет можно было бы просмотреть в стандартном Блокноте или web -броузере, переслать сотруднику, у которого ваша программа не установлена. На вкладке QReport имеются компоненты, которые позволяют это сделать. QRTextFilter - позволяет сохранить отчет в виде текстового файла. QRCSVFilter - позволяет сохранить отчет в специальном формате CSV (CommaSeparated). QRHTMLFilter - позволяет сохранить отчет в формате web -страницы. Это не визуальные компоненты, на отчете они не отобразятся. Достаточно установить один из них (или все вместе) на основу отчета QuickRep, и при сохранении отчета пользователю станут доступны соответствующие форматы: Рис. 11.10. Выбор формата сохранения отчета Причем если у вас в проекте имеется несколько окон с отчетами, компоненты добавляются только в один из них, в остальных отчетах эти форматы также станут доступны. Лекция 12. Работа с сеткой DBGrid Цель: Изучить различные способы работы с сеткой DBGrid, научить выводить текст различным цветом, добавлять в ячейки кнопки и списки. Мы с вами уже неоднократно применяли этот компонент для вывода на экран информации из наборов данных в виде таблицы. Однако этот компонент способен на большее. Профессиональные программы отличаются большим набором дополнительных возможностей для пользователя. В этой лекции мы и поговорим о дополнительных возможностях сетки DBGrid. Как мы уже знаем, строки сетки DBGrid соответствуют записям подключенного набора данных, а столбцы - полям. Свойство DataSource содержит ссылку на выбранный набор данных. Изменяя эту ссылку во время работы программы, можно изменять выводимые в сетке данные, отображая то одну, то другую таблицу (или запрос) в одной сетке DBGrid. Столбцы DBGrid Столбцы содержат значения полей подключенного к сетке набора данных. Этими значениями можно манипулировать, показывая или скрывая поля НД, меняя их местами или добавляя новые столбцы. Нам уже приходилось это делать в редакторе полей набора данных, однако, это не всегда оправдано - один набор данных может использоваться в различных местах приложения, в различных формах и на различных сетках. Изменение свойств полей набора данных в этом случае коснется и всех сеток DBGrid, которые подключены к нему, а это требуется далеко не всегда. Более разумным вариантом будет добавление всех полей в редактор полей набора данных, а изменение их свойств можно сделать в каждой сетке по-своему. Если не пользоваться редактором столбцов самой сетки, DBGrid будет выводить значения по умолчанию - будут выведены все поля набора данных, а заголовки столбцов будут соответствовать именам полей. Но стоит только добавить в редактор столбцов хоть один столбец, и сетка DBGrid будет отображать только его. Таким образом, мы можем показывать только те столбцы, которые действительно необходимы. Создайте новое приложение. Свойству Name формы, как всегда, присвойте значение fMain, свойству Caption - "Изучение свойств DBGrid". Проект сохраните в отдельную папку, модулю дайте имя Main, а проекту в целом - MyDBGrid. В эту же папку скопируйте базу данных ok.mdb из прошлой лекции. На форме нам понадобятся сетка DBGrid с вкладки DataControls, с вкладки ADO компоненты ADOConnection и ADOTable, с вкладки DataAccess - компонент DataSource. Также для красоты и удобства можно добавить компонент DBNavigator. Из прошлых лекций вы знаете, как подключить к базе данных компонент ADOConnection, а затем подключить к нему таблицу ADOTable. В свойстве TableName таблицы выберите таблицу LichData, и откройте ее. Компонент DataSource подключите к нашей таблице, а сетку DBGrid и навигатор DBNavigator - к DataSource. В результате у вас должна получиться простая форма с сеткой и навигатором по ней, в которой отображаются все поля таблицы LichData: Рис. 12.1 . Форма проекта Допустим, в нашем проекте нам нужны не все поля таблицы, а только некоторые из них. Значит, придется поработать с редактором столбцов сетки DBGrid. Вызвать редактор можно тремя способами: дважды щелкнуть по сетке; щелкнуть правой кнопкой по сетке и в контекстном меню выбрать команду ColumnsEditor и, наконец, щелкнув дважды по свойству сетки Columns в Инспекторе Объектов: Рис. 12.2 . Редактор столбцов сетки DBGrid Работа с этим редактором очень похожа на редактор полей набора данных, но есть и отличия. В верхней части окна вы видите четыре кнопки, слева - направо: 1. AddNew (Добавить новый столбец). 2. DeleteSelected (Удалить выделенный столбец). 3. AddAllFields (Добавить все столбцы из набора данных). 4. RestoreDefaults (Восстановить значения по умолчанию для выделенного столбца). Если столбцов в редакторе нет, то сетка отображает все поля НД. Добавим один столбец. Для этого нажмем первую кнопку. Сразу же все поля НД исчезли, а сетка отображает пустой столбец. Выделим его в редакторе столбцов, а в Инспекторе объектов в свойстве FieldName выберем поле "Фамилия". Сразу же столбец отобразит это поле. Заголовок столбца будет соответствовать названию поля. В нашей БД имена полей мы задавали русскими буквами, однако это бывает не всегда, особенно если вы работаете с таблицами Paradox или клиент-серверными БД. В этом случае названия полей будут выводиться латиницей, а это не удобно для пользователя. Изменить параметры заголовка столбца можно в раскрывающемся свойстве Title, которое имеет ряд собственных свойств: Alignment - свойство устанавливает выравнивание заголовка и может быть taCenter (по центру), taLeftJustify (по левому краю) и taRightJustify (по правому краю). По умолчанию, заголовок выровнен по левому краю. Caption - свойство содержит текст, который отображается в заголовке столбца. Если поле НД имеет имя латинскими буквами, именно здесь можно отобразить его кириллицей. Color - цвет заголовка, по умолчанию это свойство равно clBtnFace, что обеспечивает стандартный серый цвет. Если вы желаете украсить программу, можете выбрать другой цвет. Font - шрифт заголовка. Если дважды щелкнуть по этому свойству, откроется диалоговое окно, в котором можно изменить шрифт, начертание, размер и цвет шрифта. То же самое можно сделать, если раскрыть это свойство и непосредственно изменять нужные свойства. Надо заметить, что свойства Font, Alignment и Color внутри свойства Title меняют шрифт, выравнивание и цвет фона только заголовка столбца, а не его содержимого. Но у столбца имеются эти же свойства, они меняют шрифт, выравнивание и цвет фона выводимых в столбце данных. Свойство Visible разрешает или запрещает отображение столбца, а свойство Width позволяет изменить его ширину. О других свойствах поговорим чуть позже. Добавьте в сетку другие поля НД: "Имя", "Отчество", "Пол" и "Военнообязанный". Обратите внимание, что перетаскивая мышью столбцы в редакторе столбцов, вы можете менять их порядок в сетке. Пользователь же имеет возможность менять их порядок, перетаскивая мышью заголовки столбцов. У сетки DBGrid имеется свойство Columns, которое содержит столбцы. Щелчок по этому свойству как раз и откроет редактор столбцов сетки. Столбцы хранятся в свойстве Items в виде массива и имеют индексы от 0 до DBGrid1.Columns.Items.Count-1. Заметим, что свойство Items используется по умолчанию, и его можно не указывать: DBGrid1.Columns[0] = DBGrid1.Columns.Items[0] Шрифт и цвет можно менять не только в Инспекторе объектов, но и программно. Добавим на форму две кнопки, на которых напишем "Шрифт" и "Цвет". А также два диалога: FontDialog и ColorDialog. Создадим процедуру нажатия на первую кнопку и впишем в нее следующее: {Меняем шрифт} procedure TfMain.Button1Click(Sender: TObject); begin //считаем в диалог шрифта установленный шрифт FontDialog1.Font := DBGrid1.Columns.Items[DBGrid1.SelectedIndex].Font; //установим выбранный шрифт: if FontDialog1.Execute then DBGrid1.Columns.Items[DBGrid1.SelectedIndex].Font := FontDialog1.Font; end; Здесь вначале мы свойству Font диалога FontDialog присвоили тот же шрифт, который был в текущем столбце сетки. Затем вызвали выбор диалога, и если пользователь выбрал другой шрифт (название, начертание, размер, цвет шрифта), то изменяем шрифт всего столбца на выбранный пользователем. Аналогичным образом меняем и цвет столбца. Создайте процедуру обработки второй кнопки, и впишите код: {Меняем цвет} procedure TfMain.Button2Click(Sender: TObject); begin //считаем в диалог цвета установленный цвет: ColorDialog1.Color := DBGrid1.Columns.Items[DBGrid1.SelectedIndex].Color; //установим выбранный цвет: if ColorDialog1.Execute then DBGrid1.Columns.Items[DBGrid1.SelectedIndex].Color := ColorDialog1.Color; end; Сохраните проект, скомпилируйте его и проверьте работу кнопок. И шрифт, и цвет текущего столбца будут меняться: Рис. 12.3 . Изменение шрифта и цвета текущего столбца Следует заметить, что таким образом можно менять параметры не только содержимого текущего столбца, но и его заголовка, если, например, вместо DBGrid1.Columns.Items[DBGrid1.SelectedIndex].Color использовать DBGrid1.Columns.Items[DBGrid1.SelectedIndex].Title.Color А если применить цикл, то можно изменить параметры всех столбцов: for i:= 0 to DBGrid1.Columns.Count-1 do DBGrid1.Columns.Items[i].Color := Пустые столбцы Если добавить в редактор столбцов сетки DBGrid новый столбец, но в свойстве FieldName не выбирать поле БД, а оставить его пустым, мы получим пустой столбец. Для чего нужны пустые столбцы в сетке? Во-первых, в них можно выводить обработанные данные из других столбцов. К примеру, пользователю неудобно просматривать три столбца "Фамилия", "Имя" и "Отчество". Ему было бы удобней просмотреть один сборный столбец в формате "Фамилия И.О.". Во-вторых, пустое поле может выводить информацию по требованию. Рассмотрим эти случаи. Создайте новый столбец, но не назначайте ему поле из НД. Выделите этот столбец, и в его свойстве Title.Caption впишите "Фамилия И.О.", а в свойстве Width укажите ширину в 150 пикселей. Эти сборные данные не будут видны в момент проектирования таблицы, они выйдут только во время работы программы. Столбцы "Фамилия", "Имя" и "Отчество" нам уже не нужны, скройте их, установив их свойство Visible в False. А новый столбец перетащите мышью наверх, его индекс будет равен 0. Нам придется написать код, который нужно вписать в событие OnDrawColumnCell сетки. Это событие наступает при прорисовке каждой ячейки столбца. Также имеется событие OnDrawDataCell, которое выполняет схожие функции, но оно оставлено для поддержки старых версий, и использовать его не желательно. Итак, выделяем сетку, генерируем событие OnDrawColumnCell и вписываем код: {Прорисовка таблицы} procedure TfMain.DBGrid1DrawColumnCell(Sender: TObject; const TRect; DataCol: Integer; Column: TColumn; State: TGridDrawState); var s: String; begin //если это пустой столбец if Column.Index = 0 then begin if ADOTable1['Фамилия'] <> Null then s:= ADOTable1['Фамилия'] + ' '; if ADOTable1['Имя'] <> Null then s:= s + Copy(ADOTable1['Имя'], 1, 1) + '.'; if ADOTable1['Отчество'] <> Null then Rect: s:= s + Copy(ADOTable1['Отчество'], 1, 1)+ '.'; DBGrid1.Canvas.TextOut(Rect.Left, Rect.Top, s); end; //if end; Здесь мы вначале проверяем - наш ли это столбец (равен ли индекс нулю)? Если наш, то в переменную s начинаем собирать нужный текст. При этом имеем в виду, что пользователь мог и не заполнить некоторые поля. Чтобы у нас не произошло ошибки, вначале убеждаемся, что поле не равно Null (то есть, текст есть). Если текст есть, добавляем его в переменную s. Причем если это имя или отчество, с помощью функции Copy() получаем только первую букву и добавляем к ней точку. Когда s сформирована, добавляем этот текст в наш столбец с помощью метода TextOut() свойства Canvas сетки. В метод передаются три параметра: координаты левой позиции, верхней позиции и сам текст. Эти координаты мы берем из параметра события OnDrawColumnCell - Rect, который имеет такие свойства, как Left и Top, показывающие, соответственно, левую и верхнюю позиции текущей ячейки. В результате программа будет иметь вид: Рис. 12.4 . Заполнение пустого столбца Теперь о том, как использовать пустой столбец для вывода информации по требованию пользователя. Допустим, в сетке DBGrid нам нужна кнопка, нажатие на которую привело бы к выводу сообщения об образовании сотрудника. Создайте новый пустой столбец. Перетаскивать его не нужно, пусть будет последним. Свойство Width (ширина) установите в 20 пикселей. Название столбца ( Title.Caption ) вписывать не нужно, пусть будет пустым. В свойстве ButtonStyle выберите значение cbsEllipsis. Это приведет к тому, что при попытке редактировать этот столбец образуется кнопка с тремя точками: Рис. 12.5 . Кнопка в пустом столбце Нужный код пишется в событии OnEditButtonClick() сетки DBGrid, которое происходит всякий раз, когда пользователь нажимает на кнопку "…". Сгенерируйте это событие и впишите только одну строку: ShowMessage(ADOTable1['Образование']); Теперь, когда пользователь нажмет на эту кнопку, ему будет выведено сообщение с текстом об образовании текущего сотрудника. Список выбора в столбце Для организации списков выбора служит компонент ComboBox. Однако сетка DBGrid позволяет устроить такой же список в одном из своих столбцов без использования каких-либо других компонентов. В нашем примере есть поле "Пол". Это текстовое поле из трех символов. Во время конструирования базы данных ok.mdb с помощью программы MSAccess мы указывали, что это поле может хранить значения либо "муж", либо "жен". То же самое можно было сделать с помощью сетки. Откройте редактор столбцов сетки и выделите столбец "Пол". Обратите внимание на свойство PickList в Инспекторе объектов. Это свойство имеет тип TStrings, то есть представляет собой набор строк, так же как и свойство Items у компонента ComboBox. Щелкните дважды по PickList, чтобы открыть редактор, и впишите туда муж жен именно так, каждое значение на своей строке. Сохраните проект, скомпилируйте его и попробуйте редактировать этот столбец. При попытке редактирования в ячейке покажется похожий на ComboBox список, в котором можно будет выбрать одно из указанных значений: Рис. 12.6 . Список выбора в сетке Следует иметь в виду, что наличие такого списка не препятствует пользователю ввести какое-то иное значение. Этот список нужен не для контроля, а только для облегчения пользователю ввода данных. Если же вы не желаете, чтобы пользователь имел возможность вводить другие данные, контроль следует организовать иным способом. Еще нужно заметить, что в практике программирования список чаще формируется во время работы программы, а строки списка берутся, как правило, из другой связанной таблицы. Добавить в список новую строку очень просто: DBGrid1.Columns.Items[4].PickList.Add('абв'); Выделение отдельных строк Очень часто в практике приходится выделять какие-то строки, изменяя их фон или цвет шрифта. Например, в бухгалтерии обычно выделяют строки, в которых значение меньше нуля. Допустим, ваша программа показывает клиентов, а какой-то столбец содержит их баланс на счету вашей компании. Если этот баланс меньше 0, значит, клиент имеет задолженность перед вашей фирмой. Бухгалтеру будет очень удобно, если дебиторы (должники) будут выделяться в общем списке красным цветом. Способ прорисовки данных в сетке DBGrid зависит от значения ее свойства DefaultDrawing. По умолчанию свойство равно True, то есть данные прорисовываются автоматически. Если свойство содержит False, то прорисовку придется кодировать самостоятельно в свойствах OnDrawColumnCell или OnDrawDataCell, о которых уже упоминалось в этой лекции. Если мы написали алгоритм прорисовки, но свойство DefaultDrawing содержит True, то вначале сетка заполнится данными автоматически, а затем будет выполнен наш алгоритм. Другими словами, прорисовка некоторых частей сетки будет выполнена дважды. Это не очень хорошо для быстродействия программы, однако нам придется поступать именно так: ведь мы не все строки и столбцы собираемся выводить другим способом, а только некоторые. Остальные будут заполнены данными по умолчанию. Разберем этот метод подробней. Если найти его в справке Delphi, то увидим: propertyOnDrawColumnCell: TDrawColumnCellEvent; То есть, этот метод имеет тип TDrawColumnCellEvent. Описание типа такое: type TDrawColumnCellEvent = procedure (Sender:TObject; const Rect:TRect; DataCol:Integer; Column:TColumn; State:TGridDrawState) of object; Разберемся с параметрами. Rect - координаты прорисовки. DataCol - порядковый номер текущего столбца (начиная с 0). Column - данные текущего столбца. State - состояние ячейки. Может быть: gdSelected - ячейка выделена gdFocused - фокус ввода в ячейке gdFixed- ячейка - заголовок столбца. Допустим, в нашем примере требуется, чтобы строки с военнообязанными сотрудниками выделялись красным цветом: Рис. 12.7 . Выделение строк Заметим сразу, что наличие пустых столбцов создает дополнительные, но решаемые проблемы. Код события OnDrawColumnCell придется переделать, он будет таким: {Прорисовка таблицы} procedure TfMain.DBGrid1DrawColumnCell(Sender: TObject; const TRect; DataCol: Integer; Column: TColumn; State: TGridDrawState); var s: String; begin with DBGrid1.Canvas do begin //поле "Военнообязанный" содержит истину? if (ADOTable1['Военнообязанный'])= Trueand not (gdSelected in State) then begin //выводим все ячейки строки красным по белому: Font.Color:= clRed; FillRect(Rect); end; //if //если это пустой сборный столбец if Column.Index = 0 then begin if ADOTable1['Фамилия'] <> Null then s:= ADOTable1['Фамилия'] + ' '; if ADOTable1['Имя'] <> Null then s:= s + Copy(ADOTable1['Имя'], 1, 1) + '.'; if ADOTable1['Отчество'] <> Null then s:= s + Copy(ADOTable1['Отчество'], 1, 1)+ '.'; DBGrid1.Canvas.TextOut(Rect.Left, Rect.Top, s); end //if //если это пустой столбец с кнопкой "..." else if Column.Index = 6 then begin DBGrid1.DefaultDrawColumnCell(Rect, DataCol, Column, State); Exit; end //if //все остальные столбцы else TextOut(Rect.Left+2, Rect.Top+2, Column.Field.Text); end; //with end; Rect: Разберемся с кодом. Вначале с помощью with мы указываем, что будем работать непосредственно со свойством DBGrid1.Canvas, которое отвечает за стиль прорисовки ячейки. Далее мы смотрим, содержится ли True в поле "Военнообязанный" текущей записи. Если да, то указываем, что цвет шрифта должен быть красным, а затем функцией FillRect(Rect) мы стираем стандартный вывод. Далее мы определяем, прорисовывается ли в данный момент пустой сборный столбец с "Фамилия И.О.". Если это он, то мы формируем переменную s с нужными данными и выводим их, как в прошлый раз. Если же это пустой столбец с кнопкой "…", то мы делаем стандартный вывод и выходим из процедуры. Если мы этого не сделаем, то получим ошибку программы. Все остальные столбцы мы выводим строкой TextOut(Rect.Left+2, Rect.Top+2, Column.Field.Text); Обратите внимание, что мы добавили по два пикселя к крайней левой и крайней верхней координате ячейки. Если этого не сделать, то новая прорисовка не будет целиком закрашивать старую: Рис. 12.8 . Некорректная прорисовка Количество добавляемых пикселей зависит от формата данных и размера шрифта. Обычно это определяется путем проб. Например, ячейка может содержать цифры, которые обычно прижимаются к правому краю, и тут двумя пикселями не обойтись. Сделать проверку на тип данных можно, например, так: //если текст, сдвинем только на 2 пикселя if Column.Field.DataType = ftString then TextOut(Rect.Left, Rect.Top+2, Column.Field.Text) //если цифры, сдвинем их вправо на 28 пикселей else TextOut(Rect.Left+28, Rect.Top+2, Column.Field.Text); Выделение строки красным текстом может оказаться недостаточным для заказчика. Что, если он потребует, чтобы эти строки выделялись красной строкой с белым шрифтом? Тогда вместо Font.Color:= clRed; FillRect(Rect); вам придется написать //выводим все ячейки строки белым текстом по красному фону: Brush.Color:= clRed; Font.Color:= clWhite; FillRect(Rect); Как видите, подсвойство DBGrid1.Canvas.Brush.Color отвечает за цвет заливки ячейки, а DBGrid1.Canvas.Font.Color за цвет выводимого в ней шрифта. Список доступных цветов вы можете открыть в любом свойстве Color любого компонента. Теперь вы сможете создать сетку со сложными элементами прорисовки. Лекция 13. DBChart. Графики и диаграммы Цель: Ознакомление с компонентом DBChart, предназначенным для создания графиков и диаграмм. Изучить основные методы и свойства этого компонента, научиться программно менять внешний вид графиков. Для построения графиков и диаграмм используются компоненты Chart с вкладки Additional, и DBChart с вкладки DataControls палитры компонентов. Это равноценные компоненты, отличие состоит в том, что DBChart принимает данные из указанного набора данных - таблицы или запроса, а при использовании Chart данные приходится вносить самостоятельно. Это довольно сложные компоненты, они имеют множество свойств, которые в свою очередь сами являются сложными объектами. Если описывать компоненты графиков и диаграмм подробно, то получится небольшая книга, поэтому мы рассмотрим лишь основные приемы работы с ними. Простое приложение с графиком Графики применяются там, где нужно показать динамику подъема или спада одного или нескольких объектов. Хорошим примером является график с кривыми, демонстрирующими динамику изменений курсов доллара и евро к рублю. Диаграммы же применяются для демонстрации сравнительных показателей разных объектов. Так, например, во время предвыборной кампании часто демонстрируют диаграмму, где столбики различного роста показывают, кто из кандидатов набрал больше голосов. Круговые диаграммы показывают сравнительное отношение каждого объекта к целому, например круговая диаграмма, показывающая процент депутатов каждой партии от общего количества депутатов. Познакомимся с работой компонента DBChart на практике. Для примера нам понадобится база данных с одной таблицей, в которую мы запишем курсы доллара и евро к рублю. Таблицу вы можете сделать какой угодно - Paradox или MSAccess, в таблице создайте три поля: CDate, CDollar и CEvro. Саму таблицу назовите Curs. Заполните таблицу произвольными данными, делая по записи на каждый день, пусть в таблице будет не менее 10 записей. Данные не обязательно должны соответствовать действительности, но постарайтесь сделать их похожими на реальный курс доллара США и евро к рублю, например: CDate: 10.03.2010 CDollar: 29,21 CEvro: 40,16 Заполнив таблицу, подключите ее к новому проекту, используя технологию BDE или ADO, и сразу же переведите в True свойство Activeкомпонента TTable( TADOTable ) . Далее на форму установите пустую панель со свойством Align, равным alTop (она нам потребуется позже). На свободное место формы установите компонент DBChart с вкладки DataControls Палитры компонентов. Свойство Align компонента установите в alClient, чтобы график занял все оставшееся место формы. Теперь дважды щелкните по графику, чтобы открыть редактор серий: Рис. 13.1. Редактор серий графика Все отображаемые на графике данные построены с помощью серий - объектов Series типа TChartSeries, которые являются отображением данных отдельного реального объекта. Например, если мы используем график динамики курса доллара и евро к рублю, то серия доллара будет содержать ряд точек на графике, которые соответствуют стоимости доллара на каждый день. Для евро будет создана собственная серия. Все настройки серий можно делать как с помощью этого редактора, так и программно изменяя свойства графика. В этом примере нам потребуется сделать две серии: для доллара и для евро. Ось X будет содержать дату, а ось Y - значение. Нажмите кнопку Add (добавить серию). У вас появится окно выбора графика: Рис. 13.2. Окно выбора графика. Помимо выбора графика мы так же можем оставить или снять "галочку" 3D, которая включает или выключает объемность. Объемный график смотрится наряднее и больше подходит для всякого рода докладов или презентаций. Если же вам придется строить график в строгой деловой программе, то объемность будет разумней не использовать. Выбор типа графика или диаграммы зависит от типа отображаемых данных. В нашем примере мы собрались чертить кривую, так что нам больше подойдут типы Line или FastLine. Выберем первый из них. Как только мы это сделали, на компоненте DBChart отобразился график со случайными данными. Это происходит по умолчанию, чтобы легче было производить настройки серии. В редакторе серий появилась серия Series1. Выделите ее и щелкните по кнопке Title (заголовок). Измените заголовок на "Доллар": Рис. 13.3. Новая серия Теперь перейдите на вкладку Series на самом верху окна редактора, а уже на этой вкладке откройте внутреннюю вкладку DataSource. В выпадающем списке вы видите RandomValues (случайные значения), которые и обеспечили показ серии на графике. Нам нужно выбрать Dataset, а в окне Dataset - наш набор данных: Рис. 13.4. Подключение серии к таблице В списке Labels можно выбрать поле с данными по доллару, а можно оставить его пустым, этот список используется для отображения меток, если они установлены. В списке X выберите поле с датой, автоматически должна установиться "галочка" DateTime. Эти даты будут отображаться по оси X. А по оси Y отобразим поле с курсом доллара. Как только вы закроете редактор кнопкой Close, на форме появится график курса доллара. Далее перейдите на вкладку Chart и добавьте еще одну серию. Сделайте все точно также, только отобразите курс евро. Мы получили не очень впечатляющий график: Рис. 13.5. График курсов доллара и евро Займемся настройкой графика. Вновь откройте редактор графика и перейдите на вкладку Chart, а в ней на вкладку Titles. В выпадающем списке мы видим " Title " (заголовок графика), а в текстовом окне отображается название графика "TDBChart". Впишите вместо него "Курсы доллара и евро". Кнопка Font позволяет изменить шрифт заголовка, кнопка Border откроет окно, в котором можно настроить обрамление. Кнопка BackColor открывает диалог выбора цвета для фона заголовка, кнопка Pattern также позволяет настроить фон, придав ему цвет "родителя" - самого графика. Если вы откроете выпадающий список, то увидите, что помимо " Title " (заголовка) доступен еще и " Foot " (подвал) - надпись, которая будет выведена внизу. Напишите там "Пример простого графика". Смотрим, какие вкладки здесь еще есть. Самой последней имеется вкладка 3D, на которой можно включить или выключить объемность графика, а также отрегулировать вращение, наклон или масштаб. На вкладке Walls (стены) можно отрегулировать "стены" осей, на рисунке 13.5 они выделяются желтым и белым цветами. Вкладка Paging позволяет настроить многостраничные графики, а вкладка Panel задать параметры фона. Интересны здесь параметры панели Gradient, позволяющие задать градиентную заливку. При этом фон будет плавно переходить из одного цвета в другой. Вкладка Legend позволяет настроить легенду графика, на рисунке вы видите ее в правой части графика с надписями "Доллар" и "Евро". Перейдем на вкладку Axis (оси). Здесь мы можем сделать множество настроек осей. Вначале в левой части окна в разделе Axis нужно выбрать ось. Мы выберем Left, то есть, ось Y. Правее находится дополнительное окно со своими собственными вкладками, причем открыта вкладка Scales (шкалы): Рис. 13.6. Настройка осей графика Здесь мы снимем "галочку" Automatic, которая автоматически устанавливает размер шкалы. В большинстве случаев этого не требуется, но в нашем примере мы получили относительно ровные линии, причем одна из них в нижней, а другая - в верхней части, что не делает график красивее. Итак, снимите эту галочку, а затем с помощью кнопок Change немного увеличьте максимальное значение, и немного уменьшите минимальное. В результате кривые графика сдвинутся к середине. Далее можете перейти на внутреннюю вкладку Title, где напишите "Курс к рублю". Эта надпись является заголовком оси Y. Больше, пожалуй, с этой осью делать ничего не нужно. Зато ось X у нас вместо дат показывает значения. Исправим это. В группе радиокнопок Axis выберем Bottom (нижняя ось), и перейдем на внутреннюю вкладку Labels. В разделе Style вместо Auto выберем Value, что изменит надписи к точкам оси X: вместо назначаемых автоматически, мы четко указали, что нужно взять значение поля, то есть, дату. В результате этих манипуляций мы получим уже достаточно привлекательный график: Рис. 13.7. График курсов валют Сохраните проект, скомпилируйте и загрузите полученную программу. Посмотрим, что умеет делать этот график в рабочем приложении. Прежде всего, если вы выделите какой то участок графика левой кнопкой мыши слева-направо и сверху-вниз, выделенный фрагмент увеличится во все окно графика. Ухватившись за график правой кнопкой мыши, его можно будет перемещать. Затем вы можете сделать обратное выделение левой кнопкой любого участка графика снизу-вверх и справа-налево, и масштаб графика восстановится. Изменить масштаб программно можно с помощью свойства Zoom объекта View3DOptions графика: DBChart1.View3DOptions.Zoom:= 100; Это целое число, содержащее процент масштаба. Значение 100 соответствует нормальному масштабу. Попробуйте изменять масштаб от 1 до 500. Печать графика Посмотреть, как реализована печать графика с предварительным просмотром можно уже на этапе конструирования, в редакторе серий. Для этого на вкладке Chart редактора серий перейдите на внутреннюю вкладку General и нажмите кнопку PrintPreview. Вы получите такое окно: Рис. 13.8. Окно предварительного просмотра перед печатью В этом окне можно указать используемый принтер, если он установлен на вашем компьютере, направление печати (книжный или альбомный вариант), установить поля, произвести дополнительную настройку и непосредственно дать команду на печать. Но конечный пользователь не имеет доступа к редактору серий, поэтому нам нужно вывести это окно программным путем. Для этого установите кнопку на верхнюю панель, которую мы оставили специально для этого. На кнопке напишите "Печать". Щелкните по ней дважды, чтобы сгенерировать обработчик нажатия. Однако, прежде чем вписывать в обработчик код, нам необходимо подключить модуль Teeprevi в верхний раздел uses, потому что именно в этом модуле описана процедура ChartPreview(), вызывающая данное окно. Напомню, что для этого нужно после последнего подключенного модуля поставить запятую, после чего вписать Teeprevi а уже после этого модуля поставить точку с запятой. Код процедуры нажатия на кнопку будет содержать следующую строку: ChartPreview(Form1, DBChart1); Как видим, процедура ChartPreview() имеет два параметра: форму, содержащую график, и компонент DBChart (если вы изменили имена формы или графика, установленные по умолчанию, то и здесь нужно будет указать их). Сохраните проект, скомпилируйте его и попробуйте нажать на кнопку. У вас должно выйти окно печати. Основные методы и свойства DBChart На вкладке General, где мы вызывали окно печати, под кнопкой PrintPreview имеется кнопка Export. Эта кнопка выводит следующее диалоговое окно: Рис. 13.9. Окно экспорта графика Как мы видим, график (диаграмму) можно экспортировать в буфер обмена или графический файл одного из четырех форматов. Формат BMP наиболее универсален, но файл получается большого размера. WMF - формат метафайлов Windows, который обеспечивает хорошее качество изображения при сравнительно небольшом размере файла.EMF - такой же формат, как и WMF, но более новый, используемый в 32-разрядных Windows. Последний TEE формат - специализированный для TDBChart(TChart) формат, который сохраняет не только сам график, но и все его настройки, что в дальнейшем позволяет загрузить ранее сохраненный график в компонент TDBChart. Сохранение графика программным путем осуществляется следующими методами: Procedure SaveToBitmapFile(const Filename: String); Здесь график сохраняется в указанный в параметре файл формата BMP. Procedure SaveToMetafile(const Filename: String); График сохраняется в WMF формат. Procedure SaveToMetafileEnh(const Filename: String); График сохраняется в EMF формат. Procedure SaveChartToFile(AChart:TCustomChart; const AName: String); График сохраняется в специализированный формат. Первым параметром указывается сохраняемый график. Если вы будете использовать эту процедуру, не забудьте в раздел uses добавить модуль Teestore, где описан этот метод. Если вы сохранили график или диаграмму таким образом, то в дальнейшем вы можете и загрузить его методом Procedure LoadChartFromFile(AChart:TCustomChart; const AName: String); Следующая группа свойств позволяет вращать трехмерный график, изменять его масштаб и угол наклона, что позволяет нам в программе установить богатый инструментарий для пользователя, с помощью которого он сможет изменять вид графика. При разработке программы эти же инструменты доступны вам на вкладке Chart и на внутренней вкладке 3D. Многие возможности управления графиком зависят от того, включено ли свойство Orthogonal. При включенной ортогональности многие свойства становятся недоступными. Изменить состояние этого свойства можно просто: DBChart1.View3DOptions.Orthogonal:= False; Изменить масштаб можно, присвоив свойству Zoom целое число: DBChart1.View3DOptions.Zoom:= 300; Напомню, что нормальным масштабом является число 100. Пользователю можно дать возможность изменять масштаб от 1 до 500, еще больший масштаб будет уже неудобным. Свойство Rotation отвечает за горизонтальное вращение графика, и может быть целым числом от 0 до 360, это число указывает количество градусов: DBChart1.View3DOptions.Rotation:= 100; Свойство Tilt отвечает за вертикальное вращение и также содержит целое число от 0 до 360 (градусов): DBChart1.View3DOptions.Tilt:= 120; Свойство Elevation содержит целое число, указывающее наклон графика. Число может быть также от 0 до 360: DBChart1.View3DOptions.Elevation:= 50; Свойство Perspective указывает соответственно, на перспективу графика. Это число удобней делать от 0 до 100: DBChart1.View3DOptions.Perspective:= 30; Попробуйте сделать в проекте новое окно для настройки графика, и с помощью события onChange ползунков TrackBar изменять соответствующие свойства: Рис. 13.10. Окно настройки графика Напомню, что компонент TrackBar хранит значение в свойстве Position, а минимальное и максимальное значение можно установить при проектировании в свойствах Min и Max. Чтобы из главного окна можно было вызывать окно с настройками, а из окна настроек менять параметры графика, оба модуля придется подключить друг к другу командой File ->UseUnit. Еще раз напомню, что если ортогональность будет включена, пользователь сможет менять только масштаб графика, попытки изменить параметры вида графика ни к чему не приведут. 3. ЛАБОРАТОРНЫЕ ЗАНЯТИЯ Лабораторная работа № 1 Программирование баз данных в Delphi (Создание базы в MS Access 2007) Будем писать программу учета продуктов на складе по средней цене прихода. База данных будет включать в себя следующие возможности: 1. Справочник продуктов, справочник групп продуктов, справочник поставщиков, справочник единиц измерения. 2. Форму прихода продуктов на склад. 3. Форму расхода продуктов. 4. Возможность во всех формах которые описаны выше добавлять, редактировать и удалять записи. 5. Форму просмотра продуктов на складе. 6. Печать данных прихода, расхода и состояния склада. А для этого нам нужно научиться работать с базами данных. Начнем работу с создания базы в MS Access 2007. Создадим базу с названием storage.mdb, для этого запустим MS Acceсss 2007. Выбираем, новая база данных и вводим имя файла storage, Выбираем путь для сохранения базы данных и тип файла Базы данных Microsoft Office 2002-2003, жмем ОК, а затем нажимаем кнопку создать и видим следующее окно. Теперь создадим таблицу storage, для этого щелкнем правой кнопкой мыши на вкладке Таблица1 и выберем Конструктор. Затем вводим имя таблицы storage и жмем ОК. Далее создаем следующие поля таблицы id (счетчик) – нужно задать как ключевое, id_product (тип данных –числовой, в свойствах поля- размер поля- Длинное целое) – код продукта, id_product_group (тип данных –числовой, в свойствах поля- размер поля- Длинное целое) – код группы продуктов, quantity (тип данных -числовой, в свойствах поля- размер поля- Одинарное с плавающей точкой) – количество, price (тип данных -числовой, в свойствах поля- размер поля- Одинарное с плавающей точкой) – цена, id_prihod (тип данных –числовой, в свойствах поля- размер поля- Длинное целое) – код прихода, date_realization (тип данных - Дата/время) – дата реализации, id_ed_izmer (тип данных –числовой, в свойствах поля- размер поля- Длинное целое) – код единицы измерения. затем нажимаем на крестик и подтверждаем сохранение. Далее переходим на вкладку создание, выбираем конструктор таблиц и создаем еще пять таблиц. 1. Таблица product_group, которая содержит следующие поля: id (счетчик) – нужно задать как ключевое, product_name (текстовый) –наименование категории продукта. Данная таблица будет справочником по категориям продуктов. 2. Таблица products, которая содержит следующие поля: id (счетчик) – нужно задать как ключевое, product_name (текстовый) –наименование продукта, id_product_group (тип данных –числовой, в свойствах поля- размер поля- Длинное целое) – код группы продуктов, time_hraneniya (тип данных –числовой, в свойствах поля- размер поля- Длинное целое) – время хранения продукта, id_ed_izmer (тип данных –числовой, в свойствах поля- размер поля- Длинное целое) – код единицы измерения по умолчанию. Эта таблица будет справочником продуктов. 3. Таблица prihod, которая содержит следующие поля: id (счетчик) – нужно задать как ключевое, number_docum (текстовый, в свойствах поля- размер поля- 20) – номер накладной id_postav_ik тип данных –числовой, в свойствах поля- размер поля- Длинное целое) – код поставщика, date_prihoda (тип данных - Дата/время) – дата прихода, sum (тип данных -числовой, в свойствах поля- размер поля- Одинарное с плавающей точкой) – сумма прихода. Таблица прихода продуктов на склад. 4. Таблица ed_izmer, которая содержит следующие поля: id (счетчик) – нужно задать как ключевое, ed_name (текстовый, в свойствах поля- размер поля- 50) – наименование единицы измерения. Таблица справочник по единицам измерения. 5. Таблица postav_ik, которая содержит следующие поля: id (счетчик) – нужно задать как ключевое, postav_ik_name (текстовый, в свойствах поля- размер поля- 255) – наименование организации поставщика, phone (текстовый, в свойствах поля- размер поля- 50) – телефон организации, address (текстовый, в свойствах поля- размер поля- 255) – адрес организации, director (текстовый, в свойствах поля- размер поля- 255) – руководитель организации. Таблица будет справочником поставщиков. На этом урок закончен. Лабораторная работа № 2. Подключение к базе даных используя компонент TADOConnection. Программирование баз данных в Delphi - создаем главную форму программы склад На этом уроке мы создадим главную форму для нашего склада. Создадим подключение, к базе, используя компонент TADOConnection. Начнем… Запускаем Delphi, выбираем File -> New-> VCL Forms Application - Delphi и получаем на экране Теперь сохраним наш проект в нужную нам папку (у меня папка sclad) и назовем файл (модуля pas) как general, а файл проекта storage. Для этого выбираем File->Save Project As… Далее размещаем на форме компонент TADOConnection вкладки dbGo (старое название ADO). Перед настройкой подключения к базе, необходимо скопировать файл базы данных (storage.mdb), созданный на прошлом уроке в папку с проектом. Начинаем подключение… Выделяем компонент TADOConnection и в свойстве LoginPromt ставим на False. Это делаем для того, чтобы при подключении к БД пароль у нас не запрашивался, дальше в свойстве ConnectionString нажимаем на кнопку с «…» и видим: В окне нажимаем на кнопку «Build…» и появляется следующее окно: Выбираем провайдера, а именно Microsoft Jet 4.0 OLE DB Provaider и нажимаем кнопку «Далее». В данном окне мы указываем путь к нашей БД и имя пользователя по умолчанию Admin , жмем кнопку «Ок». Если ваша БД находится в корневом каталоге с программой, то в данном поле достаточно указать ее имя с расширением, примерно так storage.mdb. Обратите внимание, если вы укажите полный путь к базе, то при переносе на другой компьютер путь к БД будет не найден!!! В свойстве компонента TADOConnection – Mode выбрать из выпадающего списка cmShareDenyNone, а свойство TADOConnection – Connected ставим True. Все подключение к базе активировано. Сохраняем наш проект. Лабораторная работа № 3. Создание интерфейса проекта. Подключение таблицы к компоненту ADOConnection и к таблицам БД. Программирование баз данных в Delphi - создаем главную форму программы склад На этом уроке мы продолжим создание главной формы для нашего склада. Сделаем нашу форму главной MDI формой, для этого в инспекторе объектов в свойствах Form1 свойство FormStyle установим в fsMDIForm. Добавим на форму компонент TMainMenu из вкладки Standard Щелкнем дважды мышкой на компоненте MainMenu1 и увидим окно для создания меню. В свойстве Caption инспектора объектов задаем следующие пункты меню: Справочники Единицы измерения Категории продуктов Продукты Поставщики Склад Приход Расход Просмотр склада Заканчиваем создание меню и жмем на крестик. Далее из панели компонентов dbGo (ADO) размещаем на форме четыре компонента TADOTable, к ним мы подключим наши справочники из базы данных, но для начала мы их переименуем ADOTable1 – table_ed_izmer, ADOTable2 – table_product_group, ADOTable3 – table_products, ADOTable4 – table_postav_ik. Для этого в инспекторе объектов ADOTable1 в свойство Name записываем table_ed_izmer и так делаем для ADOTable2, ADOTable3, ADOTable4. Должно получиться следующее: Подключим наши таблицы к компоненту ADOConnection1 и к одноименным таблицам нашей БД. Для этого в инспекторе объектов для таблицы table_ed_izmer (TADOTable) свойства Connection устанавливаем -> ADOConnection1, TableName-> ed_izmer, Active-> True; для таблицы table_product_group (TADOTable) свойства Connection устанавливаем > ADOConnection1, TableName-> product_group, Active-> True; для таблицы table_products (TADOTable) свойства Connection устанавливаем -> ADOConnection1, TableName-> products, Active-> True; для таблицы table_postav_ik (TADOTable) свойства Connection устанавливаем -> ADOConnection1, TableName-> postav_ik , Active-> True; Свойство Active -> True устанавливайте в последнюю очередь. На последок изменим в инспекторе объектов свойство Caption главной формы (Form1) на Склад, а свойство Name ->на Form_general. А чтобы форма разворачивалась на весь экран Align -> alClient Не забываем сохранять все изменения перед завершением работы!!! Лабораторная работа № 4 Компонент TDataSource На этом уроке мы создадим форму для справочника единицы измерения нашего склада. Запускает наш проект, и создаем новую форму File->New->Form -Delphi В инспекторе объектов устанавливаем следующие свойства для новой формы Caption -> Единицы измерения; FormStyle -> fsMDIChild; Name -> Form_ed_izmer; Сохраните модуль с именем ed_izmer. Пропишем Uses general; после раздела implementation в программном модуле unit ed_izmer как показано на рисунке. А программном модуле unit general главной формы пропишем Uses ed_izmer; Это делается для того, чтобы установить связь между формами, иначе нельзя будет обращаться из одной формы к компонентам другой формы. Далее размещаем на форме следующие компоненты Три кнопки TButton из вкладки Standart В инспекторе объектов для Button1 устанавливаем свойство Caption -> Новая запись; для Button2 устанавливаем свойство Caption -> Удалить запись; для Button3 устанавливаем свойство Caption -> Закрыть; Из вкладки Data Access помещаем на форму компонент TDataSource. В инспекторе объектов для него устанавливаем следующие свойства: DataSet -> Form_general.table_ed_izmer; Name -> ed_izmer. А из вкладки Data Controls помещаем на форму компонент TDBGrid. В инспекторе объектов для него устанавливаем следующие свойства: DataSource -> ed_izmer; Если вы все правильно сделали то должны на форме увидеть следующее: Если поля таблицы не отображаются в DBGrid,возможно у вас отключено (False) свойство Active главной форме в инспекторе объектов компонента table_ed_izmer (TADOtable), установите Active -> True. Сейчас мы переименуем поля нашей таблице. Для этого перейдем на главную форму проекта. Выберем компонент table_ed_izmer (ADOTable) и в структуре (Structure) -> ed_izmer -> Fields -> щелкнем правой кнопкой мыши и выберем Add all fields. У вас должно получиться следующее: Далее выбираем поле id и в инспекторе объектов устанавливаем у него свойство Visible>False, затем выбираем поле ed_name и в инспекторе объектов устанавливаем у него свойства DisplayLabel -> Единица измерения; DisplayWidth -> 30; После этого видим для формы «Единицы измерения»: Теперь пишем обработчики событий для кнопок. Щелкнем дважды мышкой по кнопке «Новая запись» и пишем следующий код: procedure TForm_ed_izmer.Button1Click(Sender: TObject); begin //новая запись form_general.table_ed_izmer.Append; dbgrid1.SetFocus end; Щелкнем дважды мышкой по кнопке «Удалить запись» и пишем следующий код: procedure TForm_ed_izmer.Button2Click(Sender: TObject); //удаление записи begin //если записи отсутствуют то выводим сообщение, иначе удаляем запись if dbgrid1.DataSource.DataSet.RecordCount<>0 then begin if (MessageBox(Handle, 'Удаление записей может привести к нарушению работы всей базы данных. Удалить запись?', 'Внимание !!!', MB_YESNO)= IDYES) then form_general.table_ed_izmer.Delete end else ShowMessage('Нет записей для удаления'); end; Щелкнем дважды мышкой по кнопке «Закрыть» и пишем следующий код: procedure TForm_ed_izmer.Button3Click(Sender: TObject); begin //закрытие формы Close; end; В инспекторе объектов для Form_ed_izmer событие FormActivate пишем: procedure TForm_ed_izmer.FormActivate(Sender: TObject); begin //установка фокуса при активации формы dbgrid1.SetFocus; end; В инспекторе объектов для Form_ed_izmer событие FormClose пишем: procedure TForm_ed_izmer.FormClose(Sender: TObject; var Action: TCloseAction); begin //процедура закрытия формы Action:=cafree; end; В инспекторе объектов для Form_ed_izmer событие FormCloseQuery пишем: procedure TForm_ed_izmer.FormCloseQuery(Sender: TObject; var CanClose: Boolean); //проверка на наличие пустых записей перед закрытием, если такие записи найдены то отменяем //закрытие формы begin if dbgrid1.DataSource.DataSet.RecordCount<>0 then begin if not (form_general.table_ed_izmer.Locate('ed_name', '', [loCaseInsensitive,loPartialKey])) then begin form_general.table_ed_izmer.edit; form_general.table_ed_izmer.post; end else begin ShowMessage('У Вас есть не заполненные записи');CanClose:=false;dbgrid1.SetFocus;end; end; end; Сейчас сделаем запуск справочника «Единиц измерения» из главной формы. Для этого выбираем в меню Project -> Options… и видим следующее окно: Выбираем Forms и перемещаем Form_ed_izmer из Auto-create forms в Avalable forms. Переходим на главную форму, щелкаем мышкой по компоненту MainMenu1, а затем по пункту Единицы измерения И пишем следующий обработчик события: procedure TForm_general.N2Click(Sender: TObject); begin f:=0; //проверяем, активна ли наша форма if ActiveMDIChild.Caption<>'Единицы измерения' then begin //если нет то ищем ее среди неактивных и если находим, то показываем ее for i:=0 to form_general.MDIChildCount-1 do if form_general.MDIChildren[i].Caption='Единицы измерения' then begin MDIChildren[i].Show;f:=1;end; end else f:=1; //если форма еще не создана, то создаем ее if f<>1 then Tform_ed_izmer.Create(Application); end; Переменные f и I описываем в разделе var var Form_general: TForm_general; f,i:integer; //f:=1 если подчиненная форма уже запущена и f:=0 если нет Вот и все готово, запускаем проект и заполняем справочник данными, например так: Лабораторная работа № 5. Компонент TDataSource На этом уроке мы создадим форму для справочника «Категории продуктов» и «Поставщики» нашего склада. Процесс создания данных форм аналогичен созданию формы для справочника «Единицы измерения», смотри предыдущий урок. Поэтому описывать буду достаточно кратко. Создаем новую форму File->New->Form –Delphi В инспекторе объектов устанавливаем следующие свойства для новой формы Caption -> Категории продуктов; FormStyle -> fsMDIChild; Name -> Form_product_group; Сохраните модуль с именем product_group. Пропишем Uses general; после раздела implementation в программном модуле unit product_group как показано на рисунке. А программном модуле unit general главной формы пропишем Uses ed_izmer, product_group; Далее размещаем на форме следующие компоненты Три кнопки TButton из вкладки Standart В инспекторе объектов для Button1 устанавливаем свойство Caption -> Новая запись; для Button2 устанавливаем свойство Caption -> Удалить запись; для Button3 устанавливаем свойство Caption -> Закрыть. Из вкладки Data Access помещаем на форму компонент TDataSource. В инспекторе объектов для него устанавливаем следующие свойства: DataSet -> Form_general.table_product_group; Name -> product_group. А из вкладки Data Controls помещаем на форму компонент TDBGrid. В инспекторе объектов для него устанавливаем следующие свойства: DataSource -> product_group. Если поля таблицы не отображаются в DBGrid,возможно у вас отключено (False) свойство Active главной форме в инспекторе объектов компонента table_product_group (TADOtable), установите Active -> True. Сейчас мы переименуем поля нашей таблице. Для этого перейдем на главную форму проекта. Выберем компонент table_product_group (ADOTable) и в структуре (Structure) -> product_group -> Fields -> щелкнем правой кнопкой мыши и выберем Add all fields. Далее выбираем поле id и в инспекторе объектов устанавливаем у него свойство Visible>False, затем выбираем поле product_name и в инспекторе объектов устанавливаем у него свойства DisplayLabel -> Категория продуктов; DisplayWidth -> 30; После этого видим для формы «Категории продуктов»: Теперь пишем обработчики событий для кнопок. Щелкнем дважды мышкой по кнопке «Новая запись» и пишем следующий код: procedure TForm_product_group.Button1Click(Sender: TObject); begin //новая запись form_general.table_product_group.Append; dbgrid1.SetFocus; end; Щелкнем дважды мышкой по кнопке «Удалить запись» и пишем следующий код: procedure TForm_product_group.Button2Click(Sender: TObject); //удаление записи begin //если записи отсутствуют то выводим сообщение, иначе удаляем запись if dbgrid1.DataSource.DataSet.RecordCount<>0 then begin if (MessageBox(Handle, 'Удаление записей может привести к нарушению работы всей базы данных. Удалить запись?', 'Внимание !!!', MB_YESNO)= IDYES) then form_general.table_product_group.Delete end else ShowMessage('Нет записей для удаления'); end; Щелкнем дважды мышкой по кнопке «Закрыть» и пишем следующий код: procedure TForm_product_group.Button3Click(Sender: TObject); begin //закрытие формы Close; end; В инспекторе объектов для Form_product_group событие FormActivate пишем: procedure TForm_product_group.FormActivate(Sender: TObject); begin //установка фокуса при активации формы dbgrid1.SetFocus; end; В инспекторе объектов для Form_product_group событие FormClose пишем: procedure TForm_product_group.FormClose(Sender: TObject; var Action: TCloseAction); begin //процедура закрытия формы Action:=cafree; end; В инспекторе объектов для Form_product_group событие FormCloseQuery пишем: procedure TForm_product_group.FormCloseQuery(Sender: TObject; var CanClose: Boolean); //проверка на наличие пустых записей перед закрытием, если такие записи найдены то отменяем закрытие формы begin if dbgrid1.DataSource.DataSet.RecordCount<>0 then begin if not (form_general.table_product_group.Locate('product_name','',[loCaseInsensitive,loPartialKey])) then begin form_general.table_product_group.edit;form_general.table_product_group.post; end else begin ShowMessage('У Вас есть не заполненные записи');CanClose:=false;dbgrid1.SetFocus;end; end; end; Сейчас сделаем запуск справочника «Категории продуктов» из главной формы. Для этого выбираем в меню Project -> Options… Выбираем Forms и перемещаем Form_product_group из Auto-create forms в Avalable forms. Переходим на главную форму, щелкаем мышкой по компоненту MainMenu1, а затем по пункту Категории продуктов И пишем следующий обработчик события: procedure TForm_general.N3Click(Sender: TObject); begin f:=0; //проверяем, активна ли наша форма if ActiveMDIChild.Caption<>'Категории продуктов' then begin //если нет то ищем ее среди неактивных и если находим, то показываем ее for i:=0 to form_general.MDIChildCount-1 do if form_general.MDIChildren[i].Caption='Категории продуктов' then begin MDIChildren[i].Show;f:=1;end; end else f:=1; //если форма еще не создана, то создаем ее if f<>1 then Tform_product_group.Create(Application); end; Вот и все готово, запускаем проект и заполняем справочник данными, например так: Создаем форму для справочника «Поставщики» Создаем новую форму File->New->Form –Delphi В инспекторе объектов устанавливаем следующие свойства для новой формы Caption -> Поставщики; FormStyle -> fsMDIChild; Name -> Form_postav_ik; Сохраните модуль с именем postav_ik. Пропишем Uses general; после раздела implementation в программном модуле unit postav_ik,а в программном модуле unit general главной формы пропишем Uses ed_izmer, product_group, postav_ik; Размещаем на форме компоненты как показано на рисунке: Далее все делаем аналогично, как для предыдущего справочника. А я приведу лишь код для обработки событий. Для кнопки «Новая запись» procedure TForm_postav_ik.Button1Click(Sender: TObject); begin //новая запись form_general.table_postav_ik.Append; dbgrid1.SetFocus end; Для кнопки «Удалить запись» procedure TForm_postav_ik.Button2Click(Sender: TObject); //удаление записи begin //если записи отсутствуют то выводим сообщение, иначе удаляем запись if dbgrid1.DataSource.DataSet.RecordCount<>0 then begin if (MessageBox(Handle, 'Удаление записей может привести к нарушению работы всей базы данных. Удалить запись?', 'Внимание !!!', MB_YESNO)= IDYES) then form_general.table_postav_ik.Delete end else ShowMessage('Нет записей для удаления'); end; Для кнопки «Закрыть» procedure TForm_postav_ik.Button3Click(Sender: TObject); begin //закрытие формы Close; end; Для события FormActivate формы Form_postav_ik procedure TForm_postav_ik.FormActivate(Sender: TObject); begin //установка фокуса при активации формы dbgrid1.SetFocus; end; Для события FormClose формы Form_postav_ik procedure TForm_postav_ik.FormClose(Sender: TObject; var Action: TCloseAction); begin //процедура закрытия формы Action:=cafree; end; Для события FormCloseQuery формы Form_postav_ik procedure TForm_postav_ik.FormCloseQuery(Sender: TObject; var CanClose: Boolean); //проверка на наличие пустых записей перед закрытием, если такие записи найдены то отменяем закрытие формы begin if dbgrid1.DataSource.DataSet.RecordCount<>0 then begin if not (form_general.table_postav_ik.Locate('postav_ik_name','',[loCaseInsensitive,loPartialKey])) then begin form_general.table_postav_ik.edit;form_general.table_postav_ik.post; end else begin ShowMessage('У Вас есть не заполненные записи');CanClose:=false;dbgrid1.SetFocus;end; end; end; Внимание для более корректной работы по полю 'postav_ik_name' необходимо добавить проверку на null (т.е. в поле данные не вводились совсем). Для главной формы Form_general при нажатии на кнопку Категории продуктов выпадающего меню procedure TForm_general.N5Click(Sender: TObject); begin f:=0; //проверяем, активна ли наша форма if ActiveMDIChild.Caption<>'Поставщики' then begin //если нет то ищем ее среди неактивных и если находим, то показываем ее for i:=0 to form_general.MDIChildCount-1 do if form_general.MDIChildren[i].Caption='Поставщики' then begin MDIChildren[i].Show;f:=1;end; end else f:=1; //если форма еще не создана, то создаем ее if f<>1 then Tform_postav_ik.Create(Application); end; Запускаем проект и заполняем справочник данными. Лабораторная работа № 6. Компонент TDataSource Программирование баз данных в Delphi - создаем подчиненные формы для справочников программы склад. Создаем справочник «Продукты» с использованием полей Lookup. На этом уроке мы создадим форму для справочника «Продукты» нашего склада c использованием поле Lookup из других таблиц. Процесс создания данных форм аналогичен созданию формы для справочника «Единицы измерения», смотри предыдущие уроки. Поэтому описывать буду достаточно кратко. Создаем новую форму File->New->Form –Delphi В инспекторе объектов устанавливаем следующие свойства для новой формы Caption -> Продукты; FormStyle -> fsMDIChild; Name -> Form_products; Сохраните модуль с именем products. Пропишем Uses general; после раздела implementation в программном модуле unit products. А программном модуле unit general главной формы пропишем Uses ed_izmer, product_group, products; Далее размещаем на форме следующие компоненты Три кнопки TButton из вкладки Standart В инспекторе объектов для Button1 устанавливаем свойство Caption -> Новая запись; для Button2 устанавливаем свойство Caption -> Удалить запись; для Button3 устанавливаем свойство Caption -> Закрыть. Из вкладки Data Access помещаем на форму компонент TDataSource. В инспекторе объектов для него устанавливаем следующие свойства: DataSet -> Form_general.table_products; Name -> products. А из вкладки Data Controls помещаем на форму компонент TDBGrid. В инспекторе объектов для него устанавливаем следующие свойства: DataSource -> products. Если поля таблицы не отображаются в DBGrid,возможно у вас отключено (False) свойство Active главной форме в инспекторе объектов компонента table_products (TADOtable), установите Active -> True. Сейчас мы переименуем поля нашей таблице. Для этого перейдем на главную форму проекта. Выберем компонент table_products (ADOTable) и в структуре (Structure) -> products -> Fields -> щелкнем правой кнопкой мыши и выберем Add all fields. Для полей id, id_product_group, id_ed_izmer в инспекторе объектов устанавливаем свойство Visible->False, затем выбираем поле product_name и в инспекторе объектов устанавливаем у него свойства DisplayLabel -> Продукт; DisplayWidth -> 30; затем выбираем поле time_hraneniya и в инспекторе объектов устанавливаем у него свойства DisplayLabel -> Время хранения; DisplayWidth -> 20; Далее создаем выпадающее поле Lookup для просмотра данных из справочника «Категории продуктов» . Для этого в структуре (Structure) -> products -> Fields -> щелкнем правой кнопкой мыши и выберем New field…. После этого видим: В поле Name: пишем product_group; в поле Type: выбираем String; выбираем поле Lookup; в Key Fields:выбираем id_product_group; в Dataset: выбираем table_product_group; в Lookup Keys: выбираем id; а в Result Field: выбираем product_name жмем ОК В инспекторе объектов для созданного поля свойство DisplyLabel-> Категория продукта, DisplyWidth-> 24 и перемещаем поле мышкой в позицию после поля product_name. Далее создаем выпадающее поле Lookup для просмотра данных из справочника «Единицы измерения» . Для этого в структуре (Structure) -> products -> Fields -> щелкнем правой кнопкой мыши и выберем New field…. и заполняем как показано на рисунке: В поле Name: пишем ed_izmer; в поле Type: выбираем String; выбираем поле Lookup; в Key Fields:выбираем id_ed_izmer; в Dataset: выбираем table_ed_izmer; в Lookup Keys: выбираем id; а в Result Field: выбираем ed_name жмем ОК В инспекторе объектов для созданного поля свойство DisplyLabel-> Единица измерения, DisplyWidth-> 24 и перемещаем поле мышкой в позицию после поля product_group. После этого видим для формы «Продукты»: Теперь пишем обработчики событий для кнопок. Щелкнем дважды мышкой по кнопке «Новая запись» и пишем следующий код: procedure TForm_products.Button1Click(Sender: TObject); begin //новая запись form_general.table_products.Append; dbgrid1.SetFocus end; Щелкнем дважды мышкой по кнопке «Удалить запись» и пишем следующий код: procedure TForm_products.Button2Click(Sender: TObject); //удаление записи begin //если записи отсутствуют то выводим сообщение, иначе удаляем запись if dbgrid1.DataSource.DataSet.RecordCount<>0 then begin if (MessageBox(Handle, 'Удаление записей может привести к нарушению работы всей базы данных. Удалить запись?', 'Внимание !!!', MB_YESNO)= IDYES) then form_general.table_products.Delete end else ShowMessage('Нет записей для удаления'); end; Щелкнем дважды мышкой по кнопке «Закрыть» и пишем следующий код: procedure TForm_products.Button3Click(Sender: TObject); begin //закрытие формы Close; end; В инспекторе объектов для Form_products событие FormActivate пишем: procedure TForm_products.FormActivate(Sender: TObject); begin //так как таблица продукты использует данные из таблиц единицы измерения и категории продуктов //то мы должны проверить заполнены они или нет if form_general.table_ed_izmer.RecordCount=0 then begin ShowMessage('У Вас не заполнен справочник "Единиц измерения". Для продолжения работы Вам необходимо заполнить справочник.');close;end else begin if form_general.table_product_group.RecordCount=0 then begin ShowMessage('У Вас не заполнен справочник "Категории продуктов". Для продолжения работы Вам необходимо заполнить справочник.'); close;end else begin //обновляем данные form_general.table_ed_izmer.Refresh; form_general.table_product_group.Refresh; //установка фокуса при активации формы dbgrid1.SetFocus; end; end; end; В инспекторе объектов для Form_products событие FormClose пишем: procedure TForm_products.FormClose(Sender: TObject; var Action: TCloseAction); begin //процедура закрытия формы Action:=cafree; end; В инспекторе объектов для Form_products событие FormCloseQuery пишем: procedure TForm_products.FormCloseQuery(Sender: TObject; var CanClose: Boolean); begin //проверка на наличие пустых записей перед закрытием, если такие записи найдены то отменяем закрытие //формы if dbgrid1.DataSource.DataSet.RecordCount<>0 then begin //по полю продукт if (form_general.table_products.Locate('product_name','', [loCaseInsensitive,loPartialKey])) then begin ShowMessage('У Вас есть не заполненные записи в поле "Продукт"');CanClose:=false; dbgrid1.SetFocus;end else begin //по полю категория продукта if (form_general.table_products.Locate('id_product_group',null, [loCaseInsensitive,loPartialKey])) then begin ShowMessage('У Вас есть не заполненные записи в поле "Категория продукта"'); CanClose:=false;dbgrid1.SetFocus;end else begin //по полю единица измерения if (form_general.table_products.Locate('id_ed_izmer',null, [loCaseInsensitive,loPartialKey])) then begin ShowMessage('У Вас есть не заполненные записи в поле "Единицы измерения"'); CanClose:=false;dbgrid1.SetFocus;end else begin //по полю дата хранения if (form_general.table_products.Locate('time_hraneniya',null,[loCaseInsensitive,loPartialKey])) then begin ShowMessage('У Вас есть не заполненные записи в поле "Время хранения"');CanClose:=false;dbgrid1.SetFocus;end else begin form_general.table_products.edit;form_general.table_products.post; end; end; end; end; end; end; Внимание для более корректной работы по полю 'product_name' необходимо добавить проверку на null (т.е. в поле данные не вводились совсем). Сейчас сделаем запуск справочника «Продуктов» из главной формы. Для этого выбираем в меню Project -> Options… Выбираем Forms и перемещаем Form_products из Auto-create forms в Avalable forms. Переходим на главную форму, щелкаем мышкой по компоненту MainMenu1, а затем по пункту Продукты И пишем следующий обработчик события: procedure TForm_general.N4Click(Sender: TObject); begin f:=0; //проверяем, активна ли наша форма if ActiveMDIChild.Caption<>'Продукты' then begin //если нет то ищем ее среди неактивных и если находим, то показываем ее for i:=0 to form_general.MDIChildCount-1 do if form_general.MDIChildren[i].Caption='Продукты' then begin MDIChildren[i].Show;f:=1;end; end else f:=1; //если форма еще не создана, то создаем ее if f<>1 then Tform_products.Create(Application); end; Вот и все готово, запускаем проект и заполняем справочник данными. Но теперь возникает другая проблема, так наш справочник «Продуктов» связан с другими справочниками, то при удалении из них данных возникнут проблемы и со справочником «Продуктов». Поэтому в справочниках «Категории продуктов» и «Единицы измерения», пред удаление записи мы должны проверить используется ли эта запись в справочнике «Продуктов» и если используется то запретить удаление. Изменим процедуру удаления записи в справочнике «Категории продуктов» Щелкнем дважды мышкой по кнопке «Удалить запись» на форме «Категории продуктов» и пишем следующий код: procedure TForm_product_group.Button2Click(Sender: TObject); //удаление записи begin //если записи отсутствуют то выводим сообщение, иначе удаляем запись if dbgrid1.DataSource.DataSet.RecordCount<>0 then begin if (MessageBox(Handle, 'Удаление записей может привести к нарушению работы всей базы данных. Удалить запись?', 'Внимание !!!', MB_YESNO)= IDYES) then if (form_general.table_products.Locate('id_product_group', form_general.table_product_group.FieldByName('id').Value, [loCaseInsensitive, loPartialKey])) then ShowMessage('Нельзя удалить данную категорию продуктов, так как она используется в справочнике продуктов.') else form_general.table_product_group.Delete end else ShowMessage('Нет записей для удаления'); end; Изменим процедуру удаления записи в справочнике «Единицы измерения» Щелкнем дважды мышкой по кнопке «Удалить запись» на форме «Единицы измерения» и пишем следующий код: procedure TForm_ed_izmer.Button2Click(Sender: TObject); //удаление записи begin //если записи отсутствуют то выводим сообщение, иначе удаляем запись if dbgrid1.DataSource.DataSet.RecordCount<>0 then begin if (MessageBox(Handle, 'Удаление записей может привести к нарушению работы всей базы данных. Удалить запись?', 'Внимание !!!', MB_YESNO)= IDYES) then if (form_general.table_products.Locate('id_ed_izmer', form_general.table_ed_izmer.FieldByName('id').Value, [loCaseInsensitive, loPartialKey])) then ShowMessage('Нельзя удалить данную единицу измерения, так как она используется в справочнике продуктов.') else form_general.table_ed_izmer.Delete end else ShowMessage('Нет записей для удаления'); end; Лабораторная работа № 7. Создание модуля Data Module. Программирование баз данных в Delphi - создаем формы прихода продуктов на склад. На этом уроке мы создадим формы прихода продуктов на склад. Приход продуктов будет состоять из трех форм. В первой мы будем хранить данные о приходе. Во второй, данные о продуктах для каждого прихода. Третья – форма для выбора продуктов. Приступим к созданию первой формы. Создаем новую форму File->New->Form –Delphi В инспекторе объектов устанавливаем следующие свойства для новой формы Caption -> Приход; FormStyle -> fsMDIChild; Name -> Form_prihod; Сохраните модуль с именем prihod. Пропишем Uses general; после раздела implementation в программном модуле unit prihod. А программном модуле unit general главной формы пропишем Uses ed_izmer, product_group, products, prihod; Далее размещаем на форме следующие компоненты Три кнопки TButton из вкладки Standart В инспекторе объектов для Button1 устанавливаем свойство Caption -> Новая запись; для Button2 устанавливаем свойство Caption -> Удалить запись; для Button3 устанавливаем свойство Caption -> Закрыть. А из вкладки Data Controls помещаем на форму компонент TDBGrid. Сейчас сделаем запуск через меню Склад-> Приход из главной формы. Для этого выбираем в меню Project -> Options… Выбираем Forms и перемещаем Form_prihod из Auto-create forms в Avalable forms. Переходим на главную форму, щелкаем мышкой по компоненту MainMenu1, а затем по пункту Приход И пишем следующий обработчик события: И пишем следующий обработчик события: procedure TForm_general.N7Click(Sender: TObject); begin f:=0; //проверяем, активна ли наша форма if ActiveMDIChild.Caption<>'Приход' then begin //если нет то ищем ее среди неактивных и если находим, то показываем ее for i:=0 to form_general.MDIChildCount-1 do if form_general.MDIChildren[i].Caption='Приход' then begin MDIChildren[i].Show;f:=1;end; end else f:=1; //если форма еще не создана, то создаем ее if f<>1 then Tform_prihod.Create(Application); end; Далее создаем Data Module, в нем мы будем создавать подключения к таблице прихода. File->New->Form –Other… Delphi Files-> Data Module В инспекторе объектов устанавливаем для него следующие свойства Name -> dm; Сохраните модуль с именем unit_dm. Пропишем Uses general, prihod; после раздела implementation в программном модуле unit_dm. Пропишем Uses general, unit_dm; после раздела implementation в программном модуле unit prihod. Далее из панели компонентов dbGo (ADO) размещаем на форме компоненты TADOTable и TADOQuery. В инспекторе объектов для TADOQuery в свойство Name записываем ADOQuery_delete, Connection устанавливаем -> form_general.ADOConnection1. В инспекторе объектов ADOTable1 в свойство Name записываем table_prihod, Connection устанавливаем -> form_general.ADOConnection1 TableName-> prihod, Active-> True; Из вкладки Data Access помещаем на форму компонент TDataSource. В инспекторе объектов свойство Name записываем prihod, DataSet->table_prihod; На форме Приход для DBGrid1 в инспекторе объектов свойство DataSource-> dm.prihod, а свойство Options->dgRowSelect->True. После этого в DBGrid1 должны отобразиться поля таблицы table_prihod. Переходим в DataModule (unit_dm) выбираем table_prihod и в структуре (Structure) -> prihod -> Fields -> щелкнем правой кнопкой мыши и выберем Add all fields. должно получиться следующее: Далее выбираем поле id и в инспекторе объектов устанавливаем у него свойство Visible>False. Для поля id_postav_ik устанавливаем свойство Visible->False. Для поля number_docum DisplayLabel -> Номер документа; DisplayWidth -> 15; Для поля date_prihoda DisplayLabel -> Дата прихода; DisplayWidth -> 18; Для поля sum DisplayFormat->.## DisplayLabel -> Сумма; DisplayWidth -> 10; Далее создаем выпадающее поле Lookup для просмотра данных из справочника «Поставщики». Для этого в структуре (Structure) -> prihod -> Fields -> щелкнем правой кнопкой мыши и выберем New field…. и заполняем следующее окно В поле Name: пишем postav_ik; в поле Type: выбираем String; выбираем поле Lookup; в Key Fields:выбираем id_postav_ik; в Dataset: выбираем form_general.table_postav_ik; в Lookup Keys: выбираем id; а в Result Field: выбираем postav_ik_name жмем ОК В инспекторе объектов для созданного поля свойство DisplyLabel-> Поставщик, DisplyWidth-> 24 и перемещаем поле мышкой в позицию после поля id_postav_ik. После этого видим для формы «Приход»: Лабораторная работа № 8. Компонент TADOQuery. Создание запроса. Программирование баз данных в Delphi - создаем формы прихода продуктов на склад. На этом уроке мы создадим вторую форму, в которой будут отражаться данные о продуктах для каждого прихода. Приступим к созданию формы. Создаем новую форму File->New->Form –Delphi В инспекторе объектов устанавливаем следующие свойства для новой формы Name -> Form_prihod_prod; Visible->False; Сохраните модуль с именем prihod_prod. Пропишем uses general, prihod, unit_dm; после раздела implementation в программном модуле unit prihod_prod. А программном модуле unit general главной формы пропишем Uses ed_izmer, product_group, postav_ik, products, prihod, prihod_prod; А программном модуле unit prihod формы прихода (form_prihod) пропишем uses general, unit_dm, prihod_prod; Далее размещаем на форме следующие компоненты Три кнопки TButton, компонент TEdit и пять компонентов TLable из вкладки Standart В инспекторе объектов для Button1 устанавливаем свойство Caption -> Новая запись; для Button2 устанавливаем свойство Caption -> Удалить запись; для Button3 устанавливаем свойство Caption -> Закрыть; для Edit1 устанавливаем свойство Name-> docum_number, NumbersOnly->True; для Label1 устанавливаем свойство Caption -> Документ номер; для Label2 устанавливаем свойство Caption -> от; для Label3 устанавливаем свойство Caption -> Поставщик; для Label4 устанавливаем свойство Caption -> Итого на сумму для Label5 устанавливаем свойство Caption -> руб. Из вкладки Win32, компонент TDateTimePicker В инспекторе объектов для него устанавливаем свойство Format -> dd.MM.yyyy, Time -> 0:00:00; Далее из панели компонентов dbGo (ADO) размещаем на форме компоненты два TADOTable и TADOQuery. для ADOTable1 в инспекторе объектов свойство Name записываем table_copy_prihod, Connection устанавливаем -> form_general.ADOConnection1 TableName-> prihod, Active-> True; для ADOTable2 в инспекторе объектов свойство Name записываем table_storage, Connection устанавливаем -> form_general.ADOConnection1 TableName-> strorage, Active-> True; Выбираем table_storage и в структуре (Structure) -> storage -> Fields -> щелкнем правой кнопкой мыши и выберем Add all fields. должно получиться следующее: Далее выбираем поле id и в инспекторе объектов устанавливаем у него свойство Visible>False. Для поля id_product устанавливаем свойство Visible->False. Для поля id_product_group устанавливаем свойство Visible->False. Для поля id_prihod устанавливаем свойство Visible->False. Для поля id_ed_izmer устанавливаем свойство Visible->False. Для поля quantity DisplayFormat->.### DisplayLabel -> Количество; DisplayWidth -> 10; Для поля price DisplayFormat->.## DisplayLabel -> Цена; DisplayWidth -> 10; Для поля date_realization DisplayLabel -> Реализовать до:; DisplayWidth -> 18; Далее создаем выпадающее поле Lookup (поле подстановки) для просмотра данных из справочника «Продукты». Для этого в структуре (Structure) -> Storage -> Fields -> щелкнем правой кнопкой мыши и выберем New field…. и заполняем следующее окно В поле Name: пишем product; в поле Type: выбираем String; выбираем поле Lookup; в Key Fields:выбираем id_product; в Dataset: выбираем form_general.table_products; в Lookup Keys: выбираем id; а в Result Field: выбираем product_name жмем ОК В инспекторе объектов для созданного поля свойство DisplyLabel-> Продукт, DisplyWidth-> 20 и перемещаем поле мышкой в позицию после поля id_product. Далее создаем выпадающее поле Lookup (поле подстановки) для просмотра данных из справочника «Единицы измерения». Для этого в структуре (Structure) -> Storage -> Fields -> щелкнем правой кнопкой мыши и выберем New field…. и делаем аналогично предыдущему … В поле Name: пишем ed_izmer; в поле Type: выбираем String; выбираем поле Lookup; в Key Fields:выбираем id_ed_izmer; в Dataset: выбираем form_general.table_ed_izmer; в Lookup Keys: выбираем id; а в Result Field: выбираем ed_name жмем ОК В инспекторе объектов для созданного поля свойство DisplyLabel-> Ед.изм., DisplyWidth-> 20 и перемещаем поле мышкой в позицию после поля product. Далее создаем вычисляемое поле summa . Для этого в структуре (Structure) -> Storage -> Fields -> щелкнем правой кнопкой мыши и выберем New field…. и делаем аналогично предыдущему … В поле Name: пишем summa; в поле Type: выбираем Float; выбираем поле Calculated; и жмем ОК В инспекторе объектов для созданного поля свойство DisplyLabel-> Сумма., DisplyWidth->10, DisplayFormat->.## . Свяжем таблицу table_storage с таблицей table_prihod в Datamodule (dm) для этого выбираем компонент table_storage и в инспекторе объектов свойство MasterSource -> dm.prihod, MasterFields->… и в открывшемся окне выбираем в левом окне id_prihod а в правом окне id нажимаем Add. В окне JoinedFields должна появиться связь Нажимаем ОК и связь между таблицами установлена. Для table_storage устанавливаем свойство Active-> True; для ADOQuery1 в инспекторе объектов свойство Name записываем ADOQuery_summa, Connection устанавливаем -> form_general.ADOConnection1; DataSource-> dm.prihod; SQL (TString) -> в открывшемся окне вводим следующий запрос SELECT storage.id_prihod, Sum([storage].[quantity]*[storage].[price]) AS [sum] FROM storage GROUP BY storage.id_prihod HAVING (((storage.id_prihod)=:id)); далее выбираем свойство Parameters((TParameters) Создаем новый параметр Name->id Value->Type->Integer; Выбираем ADOQuery_summa и в структуре (Structure) -> ADOQuery_summa -> Fields > щелкнем правой кнопкой мыши и выберем Add all fields. Из вкладки Data Access помещаем на форму три компонента TDataSource. для DataSource1 в инспекторе объектов свойство Name записываем summa, DataSet-> ADOQuery_summa; для DataSource2 в инспекторе объектов свойство Name записываем postav_ik, DataSet-> Form_general.table_postav_ik; для DataSource3 в инспекторе объектов свойство Name записываем storage, DataSet-> table_storage; Из вкладки Data Controls помещаем на форму компоненты TDBLookupComboBox , TDBGrid и TDBEdit. для DBGrid1 в инспекторе объектов свойство DataSource-> storage, а свойство Options>dgRowSelect->True. для DBEdit1 в Name-> DBEdit_summ, DataSource->summa, DataField->sum, ReadOnly>True. для DBLookupComboBox1 в инспекторе объектов свойство Name записываем DBLookup_postav_ik, ListSource->postav_ik, ListField->postav_ik_name, KeyField->id, DataSource->dm.prihod, DataField->id_postav_ik. Далее приводим нашу форму к следующему виду: Лабораторная работа № 9. Реализация выбора данных в БД Программирование баз данных в Delphi - создаем формы прихода продуктов на склад. На этом уроке мы создадим последнюю форму для прихода продуктов. В ней будет происходить выбор продуктов прихода. Приступим к созданию формы. Создаем новую форму File->New->Form –Delphi В инспекторе объектов устанавливаем следующие свойства для новой формы Name -> Form_select_prod; Visible->False; Сохраните модуль с именем select_prod. Пропишем uses general, prihod_prod; после раздела implementation {$R *.dfm} в программном модуле unit select_prod. А программном модуле unit select_prod формы prihod_prod пропишем uses general, prihod, unit_dm, select_prod; Далее размещаем на форме следующие компоненты Две кнопки TButton шесть компонентов TLable из вкладки Standart В инспекторе объектов для Button1 устанавливаем свойство Caption -> ОК; для Button2 устанавливаем свойство Caption -> Отмена; для Label1 устанавливаем свойство Caption -> Группа; для Label2 устанавливаем свойство Caption -> Ед.изм.; для Label3 устанавливаем свойство Caption -> Количество; для Label4 устанавливаем свойство Caption -> Цена; для Label5 устанавливаем свойство Caption -> Сумма; для Label6 устанавливаем свойство Caption ->Срок реализации. Из вкладки Win32, компонент TDateTimePicker В инспекторе объектов для него устанавливаем свойство Format -> dd.MM.yyyy, Time -> 0:00:00; Из вкладки Data Access помещаем на форму три компонента TDataSource. для DataSource1 в инспекторе объектов свойство Name записываем product_group, DataSet-> Form_general.table_product_group; для DataSource2 в инспекторе объектов свойство Name записываем products, DataSet-> Form_general.table_products; для DataSource3 в инспекторе объектов свойство Name записываем ed_izmer, DataSet-> Form_general.table_ed_izmer; Из вкладки Data Controls помещаем на форму три компонента TDBLookupComboBox и три TDBEdit. для DBLookupComboBox1 в инспекторе объектов свойство Name записываем DBLookup_product_group, ListSource -> product_group , ListField-> product_name, KeyField->id, DataSource-> Form_prihod_prod.storage, DataField-> id_product_group. для DBLookupComboBox2 в инспекторе объектов свойство Name записываем DBLookup_products, DataSource-> Form_prihod_prod.storage, DataField-> product. для DBLookupComboBox3 в инспекторе объектов свойство Name записываем DBLookup_ed_izmer, DataSource-> Form_prihod_prod.storage, DataField-> ed_izmer. для DBEdit1 в Name-> quantity, DataSource-> Form_prihod_prod.storage, DataField-> quantity; для DBEdit2 в Name-> price, DataSource-> Form_prihod_prod.storage, DataField-> price; для DBEdit3 в Name-> summ, DataSource-> Form_prihod_prod.storage, DataField-> summa, ReadOnly->True. Далее располагаем компоненты формы как показано на рисунке: Лабораторная работа № 10 Процедуры для обработки событий кнопок. На этом уроке мы напишем процедуры для обработки событий для наших форм прихода, созданных на Лабораторных работах № 7 – 9. Открываем форму прихода модуль (prihod) и пишем для события OnClick кнопки Button1 (Новая запись): procedure TForm_prihod.Button1Click(Sender: TObject); begin //новая запись form_prihod_prod.Caption:='Приход: новое' ; dm.table_prihod.Edit; dm.table_prihod.append; form_prihod_prod.DateTimePicker1.Date:=sysutils.Date; form_prihod_prod.docum_number.Text:=''; form_prihod_prod.Showmodal; end; для события OnClick кнопки Button2 (Удалить запись): procedure TForm_prihod.Button2Click(Sender: TObject); begin //удаление записи if dbgrid1.DataSource.DataSet.RecordCount<>0 then begin if (MessageBox(Handle, 'Удалить запись?', 'Внимание !!!', MB_YESNO) = IDYES) then begin dm.table_prihod.Edit; form_prihod_prod.table_storage.Edit; //из таблицы storage dm.ADOQuery_delete.SQL.Clear; dm.ADOQuery_delete.DataSource:=dm.prihod; dm.ADOQuery_delete.SQL.Add('DELETE storage.*, storage.id FROM storage WHERE (((storage.id_prihod)=:id));'); dm.ADOQuery_delete.ExecSQL; dm.ADOQuery_delete.Active:=false; //из таблицы prihod dm.table_prihod.Edit; dm.table_prihod.Delete; end; end else ShowMessage('Нет записей для удаления'); end; для события OnClick кнопки Button3 (Закрыть): procedure TForm_prihod.Button3Click(Sender: TObject); begin //закрытие формы Close; end; для события OnDblClick компонента DBGrid1: procedure TForm_prihod.DBGrid1DblClick(Sender: TObject); begin //Двойной клик на записи, если записей еще нет, то создаем новую if dbgrid1.DataSource.DataSet.RecordCount=0 then begin form_prihod_prod.Caption:='Приход: новое' ; dm.table_prihod.Edit; dm.table_prihod.append; form_prihod_prod.docum_number.Text:=''; form_prihod_prod.DateTimePicker1.Date:=sysutils.Date; end //иначе заполняем форму prihod_prod текущими данными else begin dm.table_prihod.Edit; form_prihod_prod.table_storage.Edit; form_prihod_prod.table_storage.First; form_prihod_prod.docum_number.Text:= dm.table_prihod.FieldByName('number_docum').value; form_prihod_prod.DateTimePicker1.Date:= dm.table_prihod.FieldByName('date_prihoda').Value; end; //показываем форму prihod_prod form_prihod_prod.Caption:='Приход: '+dm.table_prihod.FieldByName('number_docum').AsString; form_prihod_prod.Showmodal; end; для события OnActivate формы прихода form_prihod: procedure TForm_prihod.FormActivate(Sender: TObject); begin //так как таблица приход использует данные из таблиц единицы измерения, категории продуктов, поставщики //и продукты //то мы должны проверить заполнены они или нет if form_general.table_ed_izmer.RecordCount=0 then begin ShowMessage('У Вас не заполнен справочник "Единиц измерения". Для продолжения работы Вам необходимо заполнить справочник.');close;end else begin if form_general.table_product_group.RecordCount=0 then begin ShowMessage('У Вас не заполнен справочник "Категории продуктов". Для продолжения работы Вам необходимо заполнить справочник.');close;end else begin if form_general.table_postav_ik.RecordCount=0 then begin ShowMessage('У Вас не заполнен справочник "Поставщики". Для продолжения работы Вам необходимо заполнить справочник.');close;end else begin if form_general.table_products.RecordCount=0 then begin ShowMessage('У Вас не заполнен справочник "Продукты". Для продолжения работы Вам необходимо заполнить справочник.');close;end else begin //обновляем данные form_general.table_ed_izmer.Refresh; form_general.table_product_group.Refresh; form_general.table_postav_ik.Refresh; form_general.table_products.Refresh; //установка фокуса при активации формы dbgrid1.SetFocus; end; end; end; end; end; для события OnClose формы прихода form_prihod: procedure TForm_prihod.FormClose(Sender: TObject; var Action: TCloseAction); begin //процедура закрытия формы Action:=cafree; end; Открываем форму прихода продуктов модуль (prihod_prod) и пишем для события OnClick кнопки Button1 (Новая запись): procedure TForm_prihod_prod.Button1Click(Sender: TObject); begin //новая запись if docum_number.Text='' then begin ShowMessage('Не заполнено поле "Номер документа"'); docum_number.SetFocus; end else begin if (table_copy_prihod.Lookup('number_docum', docum_number.text, 'number_docum')<>null) and (dm.table_prihod.FieldByName('number_docum').AsString<>docum_number.Text) then begin ShowMessage('Такой номер уже существует');docum_number.Text:=''; docum_number.SetFocus; end else begin if dblookup_postav_ik.Text='' then begin ShowMessage('Не заполнено поле "Поставщик"'); dblookup_postav_ik.SetFocus;end else begin if DateTimePicker1.ToString='' then begin ShowMessage('Не заполнено поле "Дата прихода"'); DateTimePicker1.SetFocus;end else begin dm.table_prihod.FieldByName('date_prihoda').Value:=DateTimePicker1.Date; dm.table_prihod.FieldByName('number_docum').Value:=docum_number.text; dm.table_prihod.Post; dm.table_prihod.edit; table_storage.Edit; table_storage.append; table_storage.FieldByName('id_prihod').Value:= dm.table_prihod.FieldByName('id').AsInteger; form_select_prod.Showmodal; end; end; end; end; end; для события OnClick кнопки Button2 (Удалить запись): procedure TForm_prihod_prod.Button2Click(Sender: TObject); begin //удалить запись if dbgrid1.DataSource.DataSet.RecordCount<>0 then begin if (MessageBox(Handle, 'Удалить запись?', 'Внимание !!!', MB_YESNO) = IDYES) then begin table_storage.Edit; table_storage.Delete; adoquery_summa.Active:=false; adoquery_summa.Active:=true; end; end else ShowMessage('Нет записей для удаления'); end; для события OnClick кнопки Button3 (Закрыть): procedure TForm_prihod_prod.Button3Click(Sender: TObject); begin //закрытие формы Close; end; для события OnDblClick компонента DBGrid1: procedure TForm_prihod_prod.DBGrid1DblClick(Sender: TObject); begin //двойной клик для редактирования записи if docum_number.Text='' then begin ShowMessage('Не заполнено поле "Номер документа"'); docum_number.SetFocus; end else begin if (table_copy_prihod.Lookup('number_docum', docum_number.text, 'number_docum')<>null) and (dm.table_prihod.FieldByName('number_docum').AsString<>docum_number.Text) then begin ShowMessage('Такой номер уже существует'); docum_number.Text:=''; docum_number.SetFocus; end else begin if dblookup_postav_ik.Text='' then begin ShowMessage('Не заполнено поле "Поставщик"'); dblookup_postav_ik.SetFocus;end else begin if DateTimePicker1.ToString='' then begin ShowMessage('Не заполнено поле "Дата прихода"'); DateTimePicker1.SetFocus;end else begin dm.table_prihod.FieldByName('date_prihoda').Value:= DateTimePicker1.Date; dm.table_prihod.FieldByName('number_docum').Value:= docum_number.text; dm.table_prihod.Post; dm.table_prihod.edit; table_storage.Edit; table_storage.FieldByName('id_prihod').Value:= dm.table_prihod.FieldByName('id').AsInteger; if table_storage.FieldByName('id_product_group').AsInteger<>0 then form_select_prod.DBLookup_product_group.KeyValue:= table_storage.FieldByName('id_product_group').Value; //открываем форму редактирования form_select_prod.Showmodal; end; end; end; end; end; для события OnActivate формы прихода продуктов form_prihod_prod: procedure TForm_prihod_prod.FormActivate(Sender: TObject); begin //пересчет итоговой суммы при активации формы adoquery_summa.Active:=false; adoquery_summa.Active:=true; end; для события OnCloseQuery формы прихода продуктов form_prihod_prod: procedure TForm_prihod_prod.FormCloseQuery(Sender: TObject; var CanClose: Boolean); begin //проверка перед закрытием формы и закрытие формы //сохранение формы if ( MessageBox(Handle, 'Cохранить документ?', 'Внимание !!!', MB_OkCANCEL)= IDOk) then begin if docum_number.Text='' then begin ShowMessage('Не заполнено поле "Номер документа"'); CanClose:=false;docum_number.SetFocus; end else begin if (table_copy_prihod.Lookup('number_docum',docum_number.Text,'number_docum')<>null) and (dm.table_prihod.FieldByName('number_docum').AsString<>docum_number.Text)then begin ShowMessage('Такой номер уже существует');CanClose:=false;docum_number.Text:=''; docum_number.SetFocus; end else begin if dblookup_postav_ik.Text='' then begin ShowMessage('Не заполнено поле "Поставщик"');CanClose:=false;dblookup_postav_ik.SetFocus;end else begin if DateTimePicker1.ToString='' then begin ShowMessage('Не заполнено поле "Дата прихода"');CanClose:=false;DateTimePicker1.SetFocus;end else begin dm.table_prihod.Edit; dm.table_prihod.FieldByName('date_prihoda').Value:=DateTimePicker1.Date; dm.table_prihod.FieldByName('number_docum').Value:=docum_number.text; dm.table_prihod.FieldByName('sum').Value:=dbedit_summa.Field.Value; dm.table_prihod.post; end; end; end; end; end //при отказе от сохранения, проверяем, если уже заполненные записи продуктов и если такие есть, то не даем закрыть форму, пока все не будет заполнено правильно else if dbgrid1.DataSource.DataSet.RecordCount<>0 then begin if docum_number.Text='' then begin ShowMessage('Не заполнено поле "Номер документа"'); CanClose:=false;docum_number.SetFocus; end else begin if (table_copy_prihod.Lookup('number_docum',docum_number.Text, 'number_docum')<>null) and (dm.table_prihod.FieldByName('number_docum').AsString <> docum_number.Text) then begin ShowMessage('Такой номер уже существует');CanClose:=false;docum_number.Text:=''; docum_number.SetFocus; end else begin if dblookup_postav_ik.Text='' then begin ShowMessage('Не заполнено поле "Поставщик"');CanClose:=false;dblookup_postav_ik.SetFocus;end else begin if DateTimePicker1.ToString='' then begin ShowMessage('Не заполнено поле "Дата прихода"');CanClose:=false;DateTimePicker1.SetFocus;end else begin dm.table_prihod.Edit; dm.table_prihod.FieldByName('date_prihoda').Value:= DateTimePicker1.Date; dm.table_prihod.FieldByName('number_docum').Value:= docum_number.text; dm.table_prihod.FieldByName('sum').Value:= dbedit_summa.Field.Value; dm.table_prihod.post; end; end; end; end; end //если записей нет, отменяем изменения else dm.table_prihod.cancel; end; для события OnCalcFields компонента table_storage: procedure TForm_prihod_prod.table_storageCalcFields(DataSet: TDataSet); begin //вычисляемое поле сумма table_storage.FieldByName('summa').Value:= table_storage.FieldByName('price').Value* table_storage.FieldByName('quantity').Value; end; Открываем форму выбора продуктов прихода модуль (select_prod) и пишем для события OnClick кнопки Button1 (OK): procedure TForm_select_prod.Button1Click(Sender: TObject); begin //Нажатие на кнопку ОК. Проверяем и сохраняем. if dblookup_products.Text='' then begin ShowMessage('Не заполнено поле "Продукт"');dblookup_products.SetFocus;end else begin if dblookup_ed_izmer.Text='' then begin ShowMessage('Не заполнено поле "Ед.изм."');dblookup_ed_izmer.SetFocus;end else begin if quantity.Text='' then begin ShowMessage('Не заполнено поле "Количество"');quantity.SetFocus;end else begin if price.Text='' then begin ShowMessage('Не заполнено поле "Цена"'); price.SetFocus;end else begin if DateTimePicker1.ToString='' then begin ShowMessage('Не заполнено поле "Срок реализации"'); DateTimePicker1.SetFocus;end else begin if ( MessageBox(Handle, 'Cохранить документ?', 'Внимание !!!', MB_OkCANCEL)= IDOk) then begin form_prihod_prod.table_storage.Edit; form_prihod_prod.table_storage.FieldByName('id_product_group').Value:= form_general.table_products.FieldByName('id_product_group').Value; form_prihod_prod.table_storage.FieldByName('date_realization').Value:= DateTimePicker1.Date; form_prihod_prod.table_storage.Post; end else form_prihod_prod.table_storage.Cancel; close; end; end; end; end; end; end; для события OnClick кнопки Button2 (Отмена): procedure TForm_select_prod.Button2Click(Sender: TObject); begin //кнопка отмена form_prihod_prod.table_storage.Cancel; form_select_prod.Close; end; для события OnClick компонента DBLookup_products: procedure TForm_select_prod.DBLookup_productsClick(Sender: TObject); begin //заполняем поля по умолчанию при выборе продукта DBLookup_ed_izmer.KeyValue:=products.DataSet.FieldByName('id_ed_izmer').Value; form_prihod_prod.table_storage.FieldByName('id_ed_izmer').Value:= dblookup_ed_izmer.KeyValue; DateTimePicker1.Date:=sysutils.Date+ products.DataSet.FieldByName('time_hraneniya').Value; end; для события OnClick компонента DBLookup_product_group: procedure TForm_select_prod.DBLookup_product_groupClick(Sender: TObject); begin //включем фильтр при выборе группы продукта form_general.table_products.Filtered:=true; form_general.table_products.Filter:='id_product_group='+ inttostr(DBLookup_product_group.KeyValue); //отключаем фильтр, если в выбранной категории нет продуктов if products.DataSet.RecordCount=0 then begin ShowMessage('Список продуктов пустой, выберите другую категорию или заполните справочник "Продукты".'); form_general.table_products.Filtered:=false; end; end; для события OnClose формы form_select_prod: procedure TForm_select_prod.FormClose(Sender: TObject; var Action: TCloseAction); begin //пересчитываем итоговую сумму и закрываем форму. form_prihod_prod.table_storage.Cancel; form_general.table_products.Filtered:=false; form_prihod_prod.adoquery_summa.Active:=false; form_prihod_prod.adoquery_summa.Active:=true; end; Открываем форму «Справочник продуктов модуль (products) и пишем для события OnClick кнопки Button2 (Удалить запись): procedure TForm_products.Button2Click(Sender: TObject); //удаление записи begin //если записи отсутствуют то выводим сообщение, иначе проверяем используется //ли данный продукт на складе, если нет то удаляем запись if dbgrid1.DataSource.DataSet.RecordCount<>0 then begin if (MessageBox(Handle, 'Удаление записей может привести к нарушению работы всей базы данных. Удалить запись?', 'Внимание !!!', MB_YESNO)= IDYES) then if (form_prihod_prod.table_storage.Locate('id_product', form_general.table_products.FieldByName('id').Value, [loCaseInsensitive,loPartialKey])) then ShowMessage('Нельзя удалить данный продукт, так как он используется в складе.') else form_general.table_products.Delete end else ShowMessage('Нет записей для удаления'); end; а в uses добавим prihod_prod; На этом создание формы прихода закончено. Можно приступать к тестированию на наличие ошибок. Лабораторная работа № 11 SQL запросы (Создаем форму для просмотра остатков продуктов на складе) На этом уроке мы дополним нашу базу еще двумя таблицами для учета информации о расходе. Создадим SQL запросы к базе для формирования остатков продуктов на складе и создадим форму для просмотра остатков продуктов по этим запросам. Прежде чем приступить к созданию формы, необходимо добавить в нашу базу две таблицы первая для хранения информации о расходных накладных (назовем ее rashod_doc, она является аналогом таблицы prihod) и вторая для хранения информации о расходе продуктов по расходной накладной (назовем ее rashod, является аналогом таблицы storage). Откроем нашу базу storage.mdb в MS Access 2007 и создадим таблицу с именем rashod_doc. (смотрите Лабораторную работу № 1) Далее создаем следующие поля таблицы rashod_doc: id (счетчик) – нужно задать как ключевое, number_docum (текстовый, в свойствах поля- размер поля- 20) – номер расходной накладной date_rashoda (тип данных - Дата/время) – дата расхода, sum (тип данных -числовой, в свойствах поля- размер поля- Одинарное с плавающей точкой) – сумма расхода. Затем создаем таблицу rashod со следующими полями: id (счетчик) – нужно задать как ключевое, id_product (тип данных –числовой, в свойствах поля- размер поля- Длинное целое) – код продукта, id_product_group (тип данных –числовой, в свойствах поля- размер поля- Длинное целое) – код группы продуктов, quantity (тип данных -числовой, в свойствах поля- размер поля- Одинарное с плавающей точкой) – количество, price (тип данных -числовой, в свойствах поля- размер поля- Одинарное с плавающей точкой) – цена, id_rashod_doc (тип данных –числовой, в свойствах поля- размер поля- Длинное целое) – код расхода. id_ed_izmer (тип данных –числовой, в свойствах поля- размер поля- Длинное целое) – код единицы измерения. Сейчас немножко о том, как это все будет работать. Для того, чтобы посмотреть остатки продуктов на складе мы создадим три запроса к нашей базе. Первый запрос будет группировать продукты из таблицы storage по полям id_product и id_ed_izmer с одновременным суммированием по количеству (поле quantity) и сумме (поле price* quantity). Второй запрос будет делать тоже, что и первый, но по таблице rashod. А третий запрос будет находить разницу между первым и вторым запросом, и выводить эти данные в нашу форму. И так приступим к созданию формы. Запускает наш проект, и создаем новую форму File>New->Form –Delphi В инспекторе объектов устанавливаем следующие свойства для новой формы Caption -> Остатки на складе; FormStyle -> fsMDIChild; Name -> Form_ostatki; Сохраните модуль с именем ostatki. Пропишем Uses general, unit_dm; после раздела implementation в программном модуле unit ostatki. А программном модуле unit general главной формы пропишем Uses ostatki; Переходим в дата модуль (unit_dm) и размещаем там три компонента TADOQuery из вкладки dbGo (ADO) и один компонент TDataSource из вкладки Data Access. Для ADOQuery1 в инспекторе объектов устанавливаем следующие свойства: Connection-> Form_general.ADOConnection1; Name-> ADOQuery_prihod; SQL-> (TStrings) -> вставляем следующий запрос SELECT storage.id_product, Sum([storage].[price]*[storage].[quantity]) AS sum_price, Sum(storage.quantity) AS [Sum-quantity], storage.id_ed_izmer FROM storage GROUP BY storage.id_product, storage.id_ed_izmer; Свойство Active устанавливаем в True; Для ADOQuery2 в инспекторе объектов устанавливаем следующие свойства: Connection-> Form_general.ADOConnection1; Name-> ADOQuery_rashod; SQL-> (TStrings) -> вставляем следующий запрос SELECT rashod.id_product, Sum([rashod].[price]*[rashod].[quantity]) AS sum_price, Sum(rashod.quantity) AS [Sum-quantity], rashod.id_ed_izmer FROM rashod GROUP BY rashod.id_product, rashod.id_ed_izmer; Свойство Active устанавливаем в True; Для ADOQuery3 в инспекторе объектов устанавливаем следующие свойства: Connection-> Form_general.ADOConnection1; Name-> ADOQuery_ostatki; SQL-> (TStrings) -> вставляем следующий запрос SELECT products.product_name, ADOQuery_prihod.id_product, [ADOQuery_prihod].[Sumquantity]-IIf([ADOQuery_rashod].[Sum-quantity]>0,[ADOQuery_rashod].[Sum-quantity],0) AS ostatok, [ADOQuery_prihod].[sum_price]IIf([ADOQuery_rashod].[sum_price]>0,[ADOQuery_rashod].[sum_price],0) AS summa, ADOQuery_prihod.id_ed_izmer, products.id_product_group, ed_izmer.ed_name FROM ((ADOQuery_prihod LEFT JOIN ADOQuery_rashod ON ADOQuery_prihod.id_product = ADOQuery_rashod.id_product) LEFT JOIN ed_izmer ON ADOQuery_prihod.id_ed_izmer = ed_izmer.id) LEFT JOIN products ON ADOQuery_prihod.id_product = products.id; Свойство Active устанавливаем в True; Для составления запросов я использовал конструктор запросов в MS Access 2007, затем затем переключался в режим SQL и копировал запрос из в наш проект в Delphi. Созданные запросы в режиме конструктора MS Access 2007 можно посмотреть в исходниках к этому уроку в файле storage.mdb. Они называются ADOQuery_ostatki, ADOQuery_prihod, ADOQuery_rashod. Для просмотра запроса в режиме конструктора в MS Access 2007 нажмите правую кнопку мыши на этом запросе и выберите конструктор. Внимание в этом уроке была обнаружена ошибка (смотрите комментарии 3-6) запросы ADOQuery_prihod, ADOQuery_rashod должны быть созданы в самой базе. Для их создания откройте базу storage.mdb в MS Access выберите создание -> конструктор запросов, закройте добавление таблиц и щелкнув правой кнопкой мыши выберите режим SQL. Далее скопируйте туда текст соответствующего запроса и сохраните его под соответствующим именем. К сожалению другого варианта исправления ошибки пока предложить не могу. Добавленные в Unit_dm компоненты ADOQuery_prihod и ADOQuery_rashod нужно удалить. Для компонента DataSource1 устанавливаем свойства: DataSet->ADOQuery_ostatki; Name->ostatki. Далее переходим к нашей форме ostatki и размещаем на ней из вкладки Data Controls компонент TDBGrid. Для него устанавливаем следующие свойства: Align-alClient; DataSource->dm.ostatki; Options->dgRowSelect->True. Сейчас мы выберем нужные нам поля из запроса ADOQuery_ostatki для отображения их в форме. Для этого перейдем в дата модуль (unit_dm) проекта и выберем компонент ADOQuery_ostatki. В структуре (Structure) -> ADOQuery_ostatki -> Fields -> щелкнем правой кнопкой мыши и выберем Add all fields. Выбираем поле product_name и в инспекторе объектов устанавливаем свойство: DisplayLabel -> Продукт; DisplayWidth -> 30; Выбираем поле ed_name и в инспекторе объектов устанавливаем свойство: DisplayLabel -> Ед.изм.; DisplayWidth -> 10; Выбираем поле id_product и в инспекторе объектов устанавливаем свойство: Visible->False; Выбираем поле ostatok и в инспекторе объектов устанавливаем свойство: DisplayFormat ->.### DisplayLabel -> Остаток; DisplayWidth -> 10; Выбираем поле summa и в инспекторе объектов устанавливаем свойство: DisplayFormat ->.## DisplayLabel ->Сумма; DisplayWidth -> 10; Выбираем поле id_ed_izmer и в инспекторе объектов устанавливаем свойство: Visible->False; Выбираем поле id_product_group и в инспекторе объектов устанавливаем свойство: Visible->False; Далее создаем вычисляемое поле sred_price для вычисления средней стоимости продуктов» . Для этого в структуре (Structure) -> products -> Fields -> щелкнем правой кнопкой мыши и выберем New field…. и заполняем, как показано на рисунке Name->sred_price; Type->Float; Calculated; жмем ОК. Выбираем созданное поле (sred_price) и в инспекторе объектов устанавливаем свойство: DisplayFormat ->.## DisplayLabel ->Средняя цена; DisplayWidth -> 10; переходим на вкладку Events и дважды жмем мышь на событии OnCalcFields; В открывшемся обработчике событий пишем следующий код: procedure Tdm.ADOQuery_ostatkiCalcFields(DataSet: TDataSet); begin //если остатки продуктов не равны 0 то вычисляем среднюю стоимость продукта if ADOQuery_ostatki.FieldByName('ostatok').Value<>0 then ADOQuery_ostatki.FieldByName('sred_price').Value:= ADOQuery_ostatki.FieldByName('summa').Value/ ADOQuery_ostatki.FieldByName('ostatok').Value else ADOQuery_ostatki.FieldByName('sred_price').Value:=0; end; Возвращаемся снова к нашей форме ostatki и в обработчике событий для события OnActivate пишем следующий код: procedure TForm_ostatki.FormActivate(Sender: TObject); begin // переоткрываем запросы при активации формы //тем самым обновляем данные на форме dm.ADOQuery_ostatki.Active:=false; dm.ADOQuery_ostatki.Active:=true; end; Для события OnClose: procedure TForm_ostatki.FormClose(Sender: TObject; var Action: TCloseAction); begin //закрываем запросы и закрываем форму dm.ADOQuery_ostatki.Active:=false; Action:=cafree; end; Сейчас сделаем запуск формы «Остатки на складе» из главной формы. Для этого выбираем в меню Project -> Options… Выбираем Forms и перемещаем Form_ostatki из Auto-create forms в Avalable forms. Переходим на главную форму, щелкаем мышкой по компоненту MainMenu1, а затем по пункту Просмотр склада И пишем следующий обработчик события: procedure TForm_general.N9Click(Sender: TObject); begin f:=0; //проверяем, активна ли наша форма if ActiveMDIChild.Caption<>'Остатки на складе' then begin //если нет то ищем ее среди неактивных и если находим, то показываем ее for i:=0 to form_general.MDIChildCount-1 do if form_general.MDIChildren[i].Caption='Остатки на складе' then begin MDIChildren[i].Show;f:=1;end; end else f:=1; //если форма еще не создана, то создаем ее if f<>1 then Tform_ostatki.Create(Application); end; Сохраняем и запускаем проект. Лабораторная работа № 12. Цветные таблицы DBGrid На этом уроке мы поговорим о создании форм расхода продуктов на складе. Учет расхода также как и учет прихода будет состоять из трех форм. В первой форме будем выводить номер накладной расхода, дату расхода и сумму расхода по накладной. Во второй форме будем выводить список продуктов выбранных для расхода с количеством, ценой и на какую сумму. Ну и в третьей форме мы будем производить выбор продуктов для учета расхода. Как вы видите, первые две формы учета расхода практически не отличаются от первых двух форм учета прихода за одним исключением из второй формы учета расхода убрано поле поставщик. Поэтому и создание этих форм чисто технически не отличается от создания форм прихода. Я например создал первые две формы простым копированием содержимого из форм прихода. Затем переименовал названия компонентов (точнее заменил слово prihod в rashod_doc, а слово storage в rashod) и подключил одноименные таблицы. А затем написал процедуры обработки форм расхода аналогичные процедурам обработки форм прихода. Поэтому если вы разобрались с написанием форм прихода на предыдущих уроках, у вас не должно возникнуть проблем и с созданием первых двух форм расхода. А вот по созданию формы выбора продуктов расхода я немного поясню. Данная форма содержит: Фильтр по категории продуктов, который активируется по событию OnClick на компоненте CheckBox и выбору категории из выпадающего списка DBLookupComboBox, подключенного к справочнику категория продуктов; Таблицу DBgrid подключенную к запросу ADOQuery_ostatki_copy (который является копией запроса ADOQuery_ostatki). В нее выводятся список остатков продуктов на складе; Пять полей DBEdit подключенных к таблице rashod. При двойном клике на поле DBGridа данные из DBGrida (Продукт, единица измерения, средняя цена автоматически заносятся в соответствующие поля, курсор устанавливается в поле количество). При нажатии кнопки ОК, данные заносятся в таблицу rashod базы данных после прохождения проверки корректности заполненных полей. При попытке ввести количество продуктов больше чем есть на складе, вызывается предупреждение. В случае отмены данные не сохраняются. Кроме того я добавил выделение зеленым цветом нулевых и красным цветом отрицательных остатков. Так как это у нас уроки по базе данных, я предлагаю создать вам все три формы для учета расхода самостоятельно. Но вы всегда можете скачать готовый исходник к уроку и использовать его как шпаргалку. Если возникнут вопросы по созданию форм для данного урока, задавайте в комментариях, попробую помочь. Лабораторная работа № 13. Экспорт данных в MS Excel На этом уроке мы реализуем печать данных прихода, расхода и состояния склада. Печать будем реализовывать через MS Excel, то есть это будет не совсем печать, а экспорт данных в MS Excel, а там пользователь может сам выбрать, печатать или сохранить отчет. В начале приготовим шаблоны MS Excel, в которые будем выводить отчеты. Шаблон ведомость по приходу продуктов питания. В шаблоне использован стиль ссылок R1C1. Включается Параметры->Формулы-> Стиль ссылок R1C1. Файл сохраняем с типом файла Шаблон Excel 97-2003. Результат работы программы: Шаблон ведомость по расходу продуктов питания. Шаблон остатки на складе. Скачать шаблоны можно вместе с исходниками в конце статьи. Далее в формы прихода и расхода внесем несколько изменений, необходимых для фильтрации документов прихода и расхода по дате. Добавим на формы прихода и расхода по два компонента TDateTimePicker из вкладки Win32 и компонент TCheckBox из вкладки Standart. Кнопку Button для вызова печати. Размещаем, как показано на рисунке: Аналогично для формы расхода. В инспекторе объектов для обеих DateTimePicker ов обнуляем свойство Time. А для события OnChange прописываем следующий код: procedure TForm_prihod.DateTimePicker1Change(Sender: TObject); begin //Проверка установленных дат if datetimepicker1.Date>datetimepicker2.Date then begin ShowMessage('Внимание. Начальная дата прихода больше конечной'); checkbox1.Checked:=false; DateTimePicker1.Date:=DateUtils.StartOfTheMonth(now); DateTimePicker2.Date:=date; end; end; procedure TForm_prihod.DateTimePicker2Change(Sender: TObject); begin //Проверка установленных дат if datetimepicker1.Date>datetimepicker2.Date then begin ShowMessage('Внимание. Начальная дата прихода больше конечной'); checkbox1.Checked:=false; DateTimePicker1.Date:=DateUtils.StartOfTheMonth(now); DateTimePicker2.Date:=date; end; end; А для события OnClick компонента CheckBox1 пишем: procedure TForm_prihod. Click(Sender: TObject); //включение фильтра begin dm.table_prihod.Filtered:=checkbox1.Checked; if checkbox1.Checked=true then dm.table_prihod.Filter:='(date_prihoda>='+datetostr(datetimepicker1.Date)+') and ('+ 'date_prihoda<='+datetostr(datetimepicker2.Date)+')'; end; Кроме того для события OnCreate формы пишем: procedure TForm_prihod.FormCreate(Sender: TObject); begin //установка начальных значений дат DateTimePicker DateTimePicker1.Date:=DateUtils.StartOfTheMonth(now); DateTimePicker2.Date:=Date;{DateUtils.EndOfTheMonth(now); } end; В модуле Uses добавляем модуль DateUtils. Аналогичные операции проделываем с формой расхода. Далее переходим в DataModule (Unit_dm) и размещаем там компонент TADOQuery из вкладки dbGo (ADO). В свойстве Name задаем имя ADOQuery_print, в свойство Connection->Form_general.ADOConnection1. Затем из вкладки Data Access размещаем компонент TDataSource в свойстве Name задаем имя print, а в свойство DataSet>ADOQuery_print. Сейчас создадим отдельный модуль для печати. Выбираем File->New->Unit – Delphi. Назовем его print. Подключение интерфейса Excel происходит через модуль comobj. Ниже привожу полный текст модуля с комментариями: unit print; interface Uses Windows, Dialogs, SysUtils, Variants, DB, Excel_TLB, comobj, unit_dm; {Внимание!!! Перед подключением модуля Excel_TLB, необходимо импортировать библиотеку Excel. Для этого выберите Component->Import Component->Import a Type Library-> находим MS Excel и следуем инструкциям} function CreateApplication(FileName:string):boolean; procedure print_prihod(date_start,date_end:TDateTime;check:boolean); procedure print_rashod(date_start,date_end:TDateTime;check:boolean); procedure print_ostatki; var exl: OleVariant; WorkBook, Sheet: Variant; implementation function CreateApplication(FileName:string):boolean; //создаем приложение excel begin try //Создаем объект интерфейса для доступа к серверу COM exl := CreateOleObject('Excel.Application'); // Отключаем реакцию Excel на события, //чтобы ускорить вывод информации exl.Application.EnableEvents := false; //Создаем книгу и обращаемся к первому листу Workbook := exl.Application.WorkBooks.Add(GetCurrentDir()+FileName); Sheet := WorkBook.WorkSheets[1]; result:=true; Except showmessage('Внимание! Произошла ошибка при создании MS Excel приложения'); result:=false; //освобождаем интерфейсы Sheet := Unassigned; WorkBook := Unassigned; exl := Unassigned; end; end; //печать прихода procedure print_prihod(date_start,date_end:TDateTime;check:boolean); var ArrayData,ArrayData1,ArrayData2: Variant; x,y,kdx,ndx,ndy,kdy,n,m,i:integer; //ndx, ndy -начало диапазона по оси х (вправо) и по оси у (вниз) //kdx, kdy -конец диапазона по оси х и по оси у // ArrayData - двухмерный массив для продуктов // ArrayData1 - двухмерный массив для единиц измерения // ArrayData2 - массив для дат begin if CreateApplication('\Шаблоны\Ведомость прихода продуктов.xlt')=false then exit; try //делаем запрос на выбор продуктов и единиц измерения прихода //и заполняем вариантный массив для продуктов и для единиц измерения dm.ADOQuery_print.Active:=false; dm.ADOQuery_print.SQL.Clear; dm.ADOQuery_print.SQL.Add('SELECT products.product_name, ed_izmer.ed_name FROM prihod LEFT JOIN ((storage LEFT JOIN products ON storage.id_product = products.id) LEFT JOIN ed_izmer'+ ' ON storage.id_ed_izmer = ed_izmer.id) ON prihod.id = storage.id_prihod GROUP BY products.product_name, ed_izmer.ed_name;'); dm.ADOQuery_print.Active:=True; y:=dm.print.DataSet.RecordCount;//количество записей по продуктам в приходе ArrayData := VarArrayCreate([1, y*2,1,1], varVariant); //двухмерный массив для продуктов ArrayData1 := VarArrayCreate([1, y*2,1,1], varVariant); //двухмерный массив для единиц измерения dm.ADOQuery_print.First; for i:=1 to y*2 do //умножаем на два так как в шаблоне для наименования продукта //используется высота ячейки в две клетки, приходится первую заполнять, а вторую пропускать begin if (i mod 2)<>0 then begin //шаг - каждый второй ArrayData[i,1] :=dm.ADOQuery_print.FieldByName('product_name').AsString; //заполняем продукт ArrayData[i+1,1] :=''; // оставляем пустой ArrayData1[i,1] :=dm.ADOQuery_print.FieldByName('ed_name').AsString; //заполняем единицу измерения ArrayData1[i+1,1] :='';// оставляем пустой if dm.ADOQuery_print.eof<>true then dm.ADOQuery_print.next; end; end; //выполняем запрос на выбор даты прихода и заполняем вариантный массив2 dm.ADOQuery_print.Active:=false; dm.ADOQuery_print.SQL.Clear; //проверяем включен ли фильтр и формируем соответствующий запрос по дате или без if check=false then dm.ADOQuery_print.SQL.Add('SELECT prihod.date_prihoda FROM prihod GROUP BY prihod.date_prihoda ORDER BY prihod.date_prihoda;') else begin dm.ADOQuery_print.Parameters.AddParameter.Name:='date1'; dm.ADOQuery_print.Parameters.ParamByName('date1').DataType:=ftDateTime; dm.ADOQuery_print.Parameters.AddParameter.Name:='date2'; dm.ADOQuery_print.Parameters.ParamByName('date2').DataType:=ftDateTime; dm.ADOQuery_print.SQL.Add('SELECT prihod.date_prihoda FROM prihod GROUP BY prihod.date_prihoda HAVING (((prihod.date_prihoda)>=:date1 and (prihod.date_prihoda)<=:date2)) ORDER BY prihod.date_prihoda; '); dm.ADOQuery_print.Parameters.ParamByName('date1').Value:=date_start; dm.ADOQuery_print.Parameters.ParamByName('date2').Value:=date_end; end; dm.ADOQuery_print.Active:=True; x:=dm.print.DataSet.RecordCount;//количество записей дат ArrayData2 := VarArrayCreate([1, x,1,1], varVariant);//массив для дат dm.ADOQuery_print.First; for i:=1 to x do begin //заполняем массив датами ArrayData2[i,1] :=dm.ADOQuery_print.FieldByName('date_prihoda').AsString; if dm.ADOQuery_print.eof<>true then dm.ADOQuery_print.next; end; // рисуем поле данных ndy:=24; ndx:=18; kdx:=18+x*4-1; kdy:=25; //выделение диапазона ячеек sheet.Range[sheet.cells[24,18],sheet.cells[25,21]].Select; //объеденение ячеек sheet.Range[sheet.cells[24,18],sheet.cells[25,21]].Merge; //рисуем поле с цифрами ndy:=23; ndx:=18; kdx:=18+x*4-1; kdy:=23; sheet.Range[sheet.cells[23,18],sheet.cells[23,21]].Select; sheet.Range[sheet.cells[23,18],sheet.cells[23,21]].Merge; Exl.Selection.HorizontalAlignment:=xlCenter; sheet.cells[23,18].value:=3; //рисуем поле с датами ndy:=17; ndx:=18; kdx:=18+x*4-1; kdy:=22; sheet.Range[sheet.cells[17,18],sheet.cells[22,21]].Select; sheet.Range[sheet.cells[17,18],sheet.cells[22,21]].Merge; exl.Selection.Orientation := 90; Exl.Selection.HorizontalAlignment:=xlCenter; Exl.Selection.VerticalAlignment:=xlCenter; //общее выделение и размножение вправо if x>1 then begin ndy:=17; ndx:=18; kdx:=18+x*4-1; kdy:=25; sheet.Range[sheet.cells[17,18],sheet.cells[25,21]].Select; //автозаполнение выделенного диапазона exl.selection.autofill(sheet.Range[sheet.cells[ndy,ndx], sheet.cells[kdy,kdx]], xlFillDefault); end; //рисуем шапку над датой ndy:=15; ndx:=18; kdx:=18+x*4-1; kdy:=16; sheet.Range[sheet.cells[15,18],sheet.cells[16,kdx]].Select; sheet.Range[sheet.cells[15,18],sheet.cells[16,kdx]].Merge; // рисуем поле итоги sheet.Range[sheet.cells[15,kdx+1],sheet.cells[22,kdx+5]].Select; sheet.Range[sheet.cells[15,kdx+1],sheet.cells[22,kdx+5]].Merge; sheet.cells[15,kdx+1].value:='Итого'; Exl.Selection.HorizontalAlignment:=xlCenter; sheet.Range[sheet.cells[23,kdx+1],sheet.cells[23,kdx+5]].Select; sheet.Range[sheet.cells[23,kdx+1],sheet.cells[23,kdx+5]].Merge; sheet.cells[23,kdx+1].value:=3+x; Exl.Selection.HorizontalAlignment:=xlCenter; sheet.Range[sheet.cells[24,kdx+1],sheet.cells[25,kdx+5]].Select; sheet.Range[sheet.cells[24,kdx+1],sheet.cells[25,kdx+5]].Merge; //вводим формулу суммы sheet.cells[24,kdx+1].value:='=SUM(RC[-'+inttostr(x*4)+']:R[1]C[-1])'; //выделяем и рисуем границы шапки таблицы sheet.Range[sheet.cells[15,18],sheet.cells[23,kdx+5]].Select; exl.Selection.Borders[xlEdgeLeft].LineStyle := xlContinuous; exl.Selection.Borders[xlEdgeLeft].Weight := xlMedium; exl.Selection.Borders[xlEdgeTop].LineStyle := xlContinuous; exl.Selection.Borders[xlEdgeTop].Weight := xlMedium; exl.Selection.Borders[xlEdgeBottom].LineStyle := xlContinuous; exl.Selection.Borders[xlEdgeBottom].Weight := xlMedium; exl.Selection.Borders[xlEdgeRight].LineStyle := xlContinuous; exl.Selection.Borders[xlEdgeRight].Weight := xlMedium; exl.Selection.Borders[xlInsideVertical].LineStyle := xlContinuous; exl.Selection.Borders[xlInsideVertical].Weight := xlMedium; exl.Selection.Borders[xlInsideHorizontal].LineStyle := xlContinuous; exl.Selection.Borders[xlInsideHorizontal].Weight := xlMedium; ndy:=24; ndx:=2; kdx:=18+x*4-1; kdy:=24+y*2-1; //общее выделение и размножение вниз if y>1 then begin sheet.Range[sheet.cells[ndy,ndx],sheet.cells[ndy+1,kdx+5]].Select; exl.selection.autofill(sheet.Range[sheet.cells[ndy,ndx], sheet.cells[kdy,kdx+5]],xlfillcopy); end; //выделяем и рисуем границы данныx sheet.Range[sheet.cells[24,18],sheet.cells[kdy,kdx+5]].Select; exl.Selection.NumberFormat:='0,000'; exl.Selection.Borders[xlEdgeTop].LineStyle := xlContinuous; exl.Selection.Borders[xlEdgeTop].Weight := xlMedium; exl.Selection.Borders[xlEdgeBottom].LineStyle := xlContinuous; exl.Selection.Borders[xlEdgeBottom].Weight := xlMedium; exl.Selection.Borders[xlEdgeRight].LineStyle := xlContinuous; exl.Selection.Borders[xlEdgeRight].Weight := xlMedium; exl.Selection.Borders[xlInsideVertical].LineStyle := xlContinuous; exl.Selection.Borders[xlInsideVertical].Weight := xlThin; exl.Selection.Borders[xlInsideHorizontal].LineStyle := xlContinuous; exl.Selection.Borders[xlInsideHorizontal].Weight := xlThin; //заполняем продуктами и еденицами измерения sheet.Range[sheet.cells[24,2],sheet.cells[kdy,14]].value:= ArrayData; sheet.Range[sheet.cells[24,15],sheet.cells[kdy,17]].value:= ArrayData1; //в зависимости от фильтра по дате заполняем шапку датами начала периода и конца периода if check=false then sheet.cells[8,27].value:=ArrayData2[1,1]+' - '+ArrayData2[x,1] else sheet.cells[8,27].value:=DateToStr(date_start)+' - '+DateToStr(date_end); // заполняем таблицу датами dm.ADOQuery_print.First; for i:=1 to x do begin sheet.cells[17,14+i*4].value:= dm.ADOQuery_print.fieldbyname('date_prihoda').AsString; if dm.ADOQuery_print.eof<>true then dm.ADOQuery_print.next; end; //запрос на выбор продукта, единицы измерения, даты прихода и суммы //формируем запрос и заполняем таблицу данными dm.ADOQuery_print.Active:=false; dm.ADOQuery_print.SQL.Clear; dm.ADOQuery_print.SQL.Add('SELECT products.product_name, ed_izmer.ed_name, prihod.date_prihoda, Sum(storage.quantity) AS [Sum-quantity]'+ ' FROM prihod LEFT JOIN ((storage LEFT JOIN products ON storage.id_product = products.id) LEFT JOIN ed_izmer ON storage.id_ed_izmer = ed_izmer.id) ON prihod.id = storage.id_prihod'+ ' GROUP BY products.product_name, ed_izmer.ed_name, prihod.date_prihoda ORDER BY prihod.date_prihoda; '); dm.ADOQuery_print.Active:=True; dm.ADOQuery_print.First; for m:=1 to y*2 do for n:=1 to x do if (m mod 2)<>0 then begin //выбираем продукт и единицу измерения и ищем совпадение по дате прихода if (dm.ADOQuery_print.Locate('product_name;ed_name;date_prihoda', VarArrayOf([ArrayData[m,1], ArrayData1[m,1],ArrayData2[n,1]]),[loCaseInsensitive, loPartialKey])) then begin sheet.cells[23+m,14+n*4].value:= dm.ADOQuery_print.fieldbyname('Sum-quantity').Value; end; end; //показываем excel exl.visible:=true; //освобождаем память и интерфейс excel ArrayData := Unassigned; ArrayData1 := Unassigned; ArrayData2 := Unassigned; Sheet := Unassigned; WorkBook := Unassigned; exl := Unassigned; Except //в случае ошибки освобождаем ресурсы showmessage('Внимание! Произошла ошибка при создании отчета'); exl.DisplayAlerts := False; // отключаем предупреждения exl.Workbooks.Close; // закроем все книги exl.Application.quit; ArrayData := Unassigned; ArrayData1 := Unassigned; ArrayData2 := Unassigned; Sheet := Unassigned; WorkBook := Unassigned; exl := Unassigned; end; end; //печать расхода procedure print_rashod(date_start,date_end:TDateTime;check:boolean); var ArrayData,ArrayData1,ArrayData2: Variant; x,y,kdx,ndx,ndy,kdy,n,m,i:integer; //ndx, ndy -начало диапазона по оси х (вправо) и по оси у (вниз) //kdx, kdy -конец диапазона по оси х и по оси у // ArrayData - двухмерный массив для продуктов // ArrayData1 - двухмерный массив для единиц измерения // ArrayData2 - массив для дат begin if CreateApplication('\Шаблоны\Ведомость расхода продуктов.xlt')=false then exit; try //делаем запрос на выбор продуктов и единиц измерения прихода //и заполняем вариантный массив для продуктов и для единиц измерения dm.ADOQuery_print.Active:=false; dm.ADOQuery_print.SQL.Clear; dm.ADOQuery_print.SQL.Add('SELECT products.product_name, ed_izmer.ed_name FROM rashod_doc LEFT JOIN ((rashod LEFT JOIN products ON rashod.id_product = products.id) LEFT JOIN ed_izmer'+ ' ON rashod.id_ed_izmer = ed_izmer.id) ON rashod_doc.id = rashod.id_rashod_doc GROUP BY products.product_name, ed_izmer.ed_name;'); dm.ADOQuery_print.Active:=True; y:=dm.print.DataSet.RecordCount;//количество записей по продуктам в приходе ArrayData := VarArrayCreate([1, y*2,1,1], varVariant); //двухмерный массив для продуктов ArrayData1 := VarArrayCreate([1, y*2,1,1], varVariant); //двухмерный массив для единиц измерения dm.ADOQuery_print.First; for i:=1 to y*2 do //умножаем на два так как в шаблоне для наименования продукта //используется высота ячейки в две клетки, приходится первую заполнять, а вторую пропускать begin if (i mod 2)<>0 then begin //шаг - каждый второй ArrayData[i,1] := dm.ADOQuery_print.FieldByName('product_name').AsString; //заполняем продукт ArrayData[i+1,1] :=''; // оставляем пустой ArrayData1[i,1] := dm.ADOQuery_print.FieldByName('ed_name').AsString; //заполняем единицу измерения ArrayData1[i+1,1] :='';// оставляем пустой if dm.ADOQuery_print.eof<>true then dm.ADOQuery_print.next; end; end; //выполняем запрос на выбор даты прихода и заполняем вариантный массив2 dm.ADOQuery_print.Active:=false; dm.ADOQuery_print.SQL.Clear; //проверяем включен ли фильтр и формируем соответствующий запрос по дате или без if check=false then dm.ADOQuery_print.SQL.Add('SELECT rashod_doc.date_rashoda FROM rashod_doc GROUP BY rashod_doc.date_rashoda ORDER BY rashod_doc.date_rashoda;') else begin dm.ADOQuery_print.Parameters.AddParameter.Name:='date1'; dm.ADOQuery_print.Parameters.ParamByName('date1').DataType:=ftDateTime; dm.ADOQuery_print.Parameters.AddParameter.Name:='date2'; dm.ADOQuery_print.Parameters.ParamByName('date2').DataType:=ftDateTime; dm.ADOQuery_print.SQL.Add('SELECT rashod_doc.date_rashoda FROM rashod_doc GROUP BY rashod_doc.date_rashoda HAVING (((rashod_doc.date_rashoda)>=:date1 and (rashod_doc.date_rashoda)<=:date2)) ORDER BY rashod_doc.date_rashoda; '); dm.ADOQuery_print.Parameters.ParamByName('date1').Value:=date_start; dm.ADOQuery_print.Parameters.ParamByName('date2').Value:=date_end; end; dm.ADOQuery_print.Active:=True; x:=dm.print.DataSet.RecordCount;//количество записей дат ArrayData2 := VarArrayCreate([1, x,1,1], varVariant);//массив для дат dm.ADOQuery_print.First; for i:=1 to x do begin //заполняем массив датами ArrayData2[i,1] :=dm.ADOQuery_print.FieldByName('date_rashoda').AsString; if dm.ADOQuery_print.eof<>true then dm.ADOQuery_print.next; end; // рисуем поле данных ndy:=24; ndx:=18; kdx:=18+x*4-1; kdy:=25; //выделение диапазона ячеек sheet.Range[sheet.cells[24,18],sheet.cells[25,21]].Select; //объеденение ячеек sheet.Range[sheet.cells[24,18],sheet.cells[25,21]].Merge; //рисуем поле с цифрами ndy:=23; ndx:=18; kdx:=18+x*4-1; kdy:=23; sheet.Range[sheet.cells[23,18],sheet.cells[23,21]].Select; sheet.Range[sheet.cells[23,18],sheet.cells[23,21]].Merge; Exl.Selection.HorizontalAlignment:=xlCenter; sheet.cells[23,18].value:=3; //рисуем поле с датами ndy:=17; ndx:=18; kdx:=18+x*4-1; kdy:=22; sheet.Range[sheet.cells[17,18],sheet.cells[22,21]].Select; sheet.Range[sheet.cells[17,18],sheet.cells[22,21]].Merge; exl.Selection.Orientation := 90; Exl.Selection.HorizontalAlignment:=xlCenter; Exl.Selection.VerticalAlignment:=xlCenter; //общее выделение и размножение вправо if x>1 then begin ndy:=17; ndx:=18; kdx:=18+x*4-1; kdy:=25; sheet.Range[sheet.cells[17,18],sheet.cells[25,21]].Select; //автозаполнение выделенного диапазона exl.selection.autofill(sheet.Range[sheet.cells[ndy,ndx], sheet.cells[kdy,kdx]], xlFillDefault); end; //рисуем шапку над датой ndy:=15; ndx:=18; kdx:=18+x*4-1; kdy:=16; sheet.Range[sheet.cells[15,18],sheet.cells[16,kdx]].Select; sheet.Range[sheet.cells[15,18],sheet.cells[16,kdx]].Merge; // рисуем поле итоги sheet.Range[sheet.cells[15,kdx+1],sheet.cells[22,kdx+5]].Select; sheet.Range[sheet.cells[15,kdx+1],sheet.cells[22,kdx+5]].Merge; sheet.cells[15,kdx+1].value:='Итого'; Exl.Selection.HorizontalAlignment:=xlCenter; sheet.Range[sheet.cells[23,kdx+1],sheet.cells[23,kdx+5]].Select; sheet.Range[sheet.cells[23,kdx+1],sheet.cells[23,kdx+5]].Merge; sheet.cells[23,kdx+1].value:=3+x; Exl.Selection.HorizontalAlignment:=xlCenter; sheet.Range[sheet.cells[24,kdx+1],sheet.cells[25,kdx+5]].Select; sheet.Range[sheet.cells[24,kdx+1],sheet.cells[25,kdx+5]].Merge; //вводим формулу суммы sheet.cells[24,kdx+1].value:='=SUM(RC[-'+inttostr(x*4)+']:R[1]C[-1])'; //выделяем и рисуем границы шапки таблицы sheet.Range[sheet.cells[15,18],sheet.cells[23,kdx+5]].Select; exl.Selection.Borders[xlEdgeLeft].LineStyle := xlContinuous; exl.Selection.Borders[xlEdgeLeft].Weight := xlMedium; exl.Selection.Borders[xlEdgeTop].LineStyle := xlContinuous; exl.Selection.Borders[xlEdgeTop].Weight := xlMedium; exl.Selection.Borders[xlEdgeBottom].LineStyle := xlContinuous; exl.Selection.Borders[xlEdgeBottom].Weight := xlMedium; exl.Selection.Borders[xlEdgeRight].LineStyle := xlContinuous; exl.Selection.Borders[xlEdgeRight].Weight := xlMedium; exl.Selection.Borders[xlInsideVertical].LineStyle := xlContinuous; exl.Selection.Borders[xlInsideVertical].Weight := xlMedium; exl.Selection.Borders[xlInsideHorizontal].LineStyle := xlContinuous; exl.Selection.Borders[xlInsideHorizontal].Weight := xlMedium; ndy:=24; ndx:=2; kdx:=18+x*4-1; kdy:=24+y*2-1; //общее выделение и размножение вниз if y>1 then begin sheet.Range[sheet.cells[ndy,ndx], sheet.cells[ndy+1,kdx+5]].Select; exl.selection.autofill(sheet.Range[sheet.cells[ndy,ndx], sheet.cells[kdy,kdx+5]], xlfillcopy); end; //выделяем и рисуем границы данныx sheet.Range[sheet.cells[24,18],sheet.cells[kdy,kdx+5]].Select; exl.Selection.NumberFormat:='0,000'; exl.Selection.Borders[xlEdgeTop].LineStyle := xlContinuous; exl.Selection.Borders[xlEdgeTop].Weight := xlMedium; exl.Selection.Borders[xlEdgeBottom].LineStyle := xlContinuous; exl.Selection.Borders[xlEdgeBottom].Weight := xlMedium; exl.Selection.Borders[xlEdgeRight].LineStyle := xlContinuous; exl.Selection.Borders[xlEdgeRight].Weight := xlMedium; exl.Selection.Borders[xlInsideVertical].LineStyle := xlContinuous; exl.Selection.Borders[xlInsideVertical].Weight := xlThin; exl.Selection.Borders[xlInsideHorizontal].LineStyle := xlContinuous; exl.Selection.Borders[xlInsideHorizontal].Weight := xlThin; //заполняем продуктами и еденицами измерения sheet.Range[sheet.cells[24,2],sheet.cells[kdy,14]].value:= ArrayData; sheet.Range[sheet.cells[24,15],sheet.cells[kdy,17]].value:= ArrayData1; //в зависимости от фильтра по дате заполняем шапку датами начала периода и конца периода if check=false then sheet.cells[8,27].value:= ArrayData2[1,1]+' - '+ ArrayData2[x,1] else sheet.cells[8,27].value:= DateToStr(date_start) + ' - ' + DateToStr(date_end); // заполняем таблицу датами dm.ADOQuery_print.First; for i:=1 to x do begin sheet.cells[17,14+i*4].value:= dm.ADOQuery_print.fieldbyname('date_rashoda').AsString; if dm.ADOQuery_print.eof<>true then dm.ADOQuery_print.next; end; //запрос на выбор продукта, единицы измерения, даты расхода и суммы //формируем запрос и заполняем таблицу данными dm.ADOQuery_print.Active:=false; dm.ADOQuery_print.SQL.Clear; dm.ADOQuery_print.SQL.Add('SELECT products.product_name, ed_izmer.ed_name, rashod_doc.date_rashoda, Sum(rashod.quantity) AS [Sum-quantity]'+ ' FROM rashod_doc LEFT JOIN ((rashod LEFT JOIN products ON rashod.id_product = products.id) LEFT JOIN ed_izmer ON rashod.id_ed_izmer = ed_izmer.id) ON rashod_doc.id = rashod.id_rashod_doc'+ ' GROUP BY products.product_name, ed_izmer.ed_name, rashod_doc.date_rashoda ORDER BY rashod_doc.date_rashoda; '); dm.ADOQuery_print.Active:=True; dm.ADOQuery_print.First; for m:=1 to y*2 do for n:=1 to x do if (m mod 2)<>0 then begin //выбираем продукт и единицу измерения и ищем совпадение по дате прихода if (dm.ADOQuery_print.Locate('product_name;ed_name;date_rashoda', VarArrayOf([ArrayData[m,1], ArrayData1[m,1], ArrayData2[n,1]]), [loCaseInsensitive, loPartialKey])) then begin sheet.cells[23+m,14+n*4].value:=dm.ADOQuery_print.fieldbyname('Sum-quantity').Value; end; end; //показываем excel exl.visible:=true; //освобождаем память и интерфейс excel ArrayData := Unassigned; ArrayData1 := Unassigned; ArrayData2 := Unassigned; Sheet := Unassigned; WorkBook := Unassigned; exl := Unassigned; Except //в случае ошибки освобождаем ресурсы showmessage('Внимание! Произошла ошибка при создании отчета'); exl.DisplayAlerts := False; // отключаем предупреждения exl.Workbooks.Close; // закроем все книги exl.Application.quit; ArrayData := Unassigned; ArrayData1 := Unassigned; ArrayData2 := Unassigned; Sheet := Unassigned; WorkBook := Unassigned; exl := Unassigned; end; end; //Печать остатков на складе procedure print_ostatki; var ArrayData: Variant; i:integer; begin //если остатков нет выходим if dm.ostatki.DataSet.RecordCount=0 then begin showmessage('На складе нет остатков'); exit; end; //создаем интерфейс Excel if CreateApplication('\Шаблоны\Остатки на складе.xlt')=false then exit; try begin //объявляем вариантный массив ArrayData := VarArrayCreate([1, dm.ostatki.DataSet.RecordCount,1,6], varVariant); dm.ADOQuery_ostatki.First; for i:= 1 to dm.ostatki.DataSet.RecordCount do begin //Заполняем вариантный массив данными из запроса остатки ArrayData[i,1] :=i; ArrayData[i,2] :=dm.ADOQuery_ostatki.FieldByName('product_name').Value; ArrayData[i,3] :=dm.ADOQuery_ostatki.FieldByName('ed_name').Value; ArrayData[i,4] :=dm.ADOQuery_ostatki.FieldByName('ostatok').Value; ArrayData[i,5] :=dm.ADOQuery_ostatki.FieldByName('summa').Value; ArrayData[i,6] :=dm.ADOQuery_ostatki.FieldByName('sred_price').Value; dm.ADOQuery_ostatki.Next; end; end; //выделяем и рисуем границы данныx sheet.Range['a4','f'+IntToStr(dm.ostatki.DataSet.RecordCount+3)].Select; exl.Selection.Borders[xlEdgeTop].LineStyle := xlContinuous; exl.Selection.Borders[xlEdgeTop].Weight := xlMedium; exl.Selection.Borders[xlEdgeBottom].LineStyle := xlContinuous; exl.Selection.Borders[xlEdgeBottom].Weight := xlMedium; exl.Selection.Borders[xlEdgeRight].LineStyle := xlContinuous; exl.Selection.Borders[xlEdgeRight].Weight := xlMedium; exl.Selection.Borders[xlEdgeLeft].LineStyle := xlContinuous; exl.Selection.Borders[xlEdgeLeft].Weight := xlMedium; exl.Selection.Borders[xlInsideVertical].LineStyle := xlContinuous; exl.Selection.Borders[xlInsideVertical].Weight := xlThin; exl.Selection.Borders[xlInsideHorizontal].LineStyle := xlContinuous; exl.Selection.Borders[xlInsideHorizontal].Weight := xlThin; //заполняем ячейки таблицы Excel из массива sheet.Range['a4','f'+IntToStr(dm.ostatki.DataSet.RecordCount+3)].value:= ArrayData; //показываем excel exl.visible:=true; //освобождаем память и интерфейс excel ArrayData := Unassigned; Sheet := Unassigned; WorkBook := Unassigned; exl := Unassigned; Except //в случае ошибки освобождаем ресурсы showmessage('Внимание! Произошла ошибка при создании отчета'); exl.DisplayAlerts := False; // отключаем предупреждения exl.Workbooks.Close; // закроем все книги exl.Application.quit; ArrayData := Unassigned; Sheet := Unassigned; WorkBook := Unassigned; exl := Unassigned; end; end; end. Как работать с Excel из Delphi, можно прочитать здесь, или найти в Интернете. На форме приход дважды нажимаем на кнопку печат ь и пишем следующий код для события OnClick: procedure TForm_prihod.Button4Click(Sender: TObject); var date_start:TDate; date_end:TDate; check:boolean; begin //вызов процедуры печати if CheckBox1.Checked=True then check:=true else check:=false ; date_start:=(datetimepicker1.Date); date_end:=(datetimepicker2.Date); print_prihod(date_start,date_end,check); end; Не забываем прописать модуль print в uses для формы прихода. uses general, unit_dm, prihod_prod, print; Аналогично делаем и для формы расхода. Для печати остатков на складе, размещаем на форме кнопку называем ее Печать и по событию OnClick вызываем процедуру печати print_ostatki; Печать справочников вы можете организовать самостоятельно по аналогии с печатью остатков на складе. Примеры запросов к базе, созданные в конструкторе MS Access, для выбора информации для отчета по приходу я оставил в базе для образца их можно удалить. Запросы называются : ADOQuery_print1 - выбор продуктов и единиц измерения; ADOQuery_print2 – запрос на выбор дат прихода ADOQuery_print3 – запрос на выбор продукта, единицы измерения, даты прихода и суммы. Лабораторная работа № 14. Модуль для сжатия и восстановления базы данных Многие читатели моих уроков заметили, что при работе с базой данных Microsoft Access (формат mdb) очень быстро растет ее размер, хотя сама база практически пустая. И мне на почту приходят вопросы, как сжать базу. На сегодняшнем уроке я расскажу, как программно сжать базу данных Microsoft Access 2003-2007. Ну во первых операцию по сжатью базы можно выполнить в ручную в самой программе Microsoft Access 2007. Для этого необходимо выполнить действия, показанные на рисунке: Но нас интересует программный путь. Для этого запускаем наш проект и переходим на главную форму (form_general). Дважды кликаем мышью на компоненте MainMenu1 и создаем пункт меню «Сервис» и подпункт «Сжатие и восстановление базы»: Затем дважды кликаем на пункте «Сжатие и восстановление базы» и пишем следующий код: procedure TForm_general.N11Click(Sender: TObject); Var path,db,db1:string; begin //закрываем формы if (form_general.MDIChildCount)<>0 then begin ShowMessage('Закройте все формы и повторите операцию.'); exit; end; if MessageBox(Handle, 'Вы приступили к сжатию и восстановлению базы данных. Продолжить? ', 'Внимание !!!', MB_YESNO)<> IDYES then exit; //извлекаем путь к базе, в нашем случае это папка с нашим приложением path:=ExtractFilePath(Application.ExeName); db:=path+'storage.mdb'; //закрываем соединение с базой for i:= 0 to ADOConnection1.DataSetCount - 1 do ADOConnection1.DataSets[i].Close; ADOConnection1.Connected:=False; //вызываем процедуру сжатия и восстановления базы CompactDatabase_JRO(db,'',''); end; Изобретать велосипед я не буду, а возьму готовый код приведенный ниже: Для его работы добавим в Uses модуль ComObj; uses ……ComObj; В разделе type процедуру Procedure CompactDatabase_JRO(DatabaseName: String; DestDatabaseName: String=''; Password:String=''), собственно которая и будет сжимать базу. type ………………….. ………………….. Procedure CompactDatabase_JRO(DatabaseName: String; DestDatabaseName: String=''; Password:String=''); В разделе implementation добавим код самой процедуры: implementation {$R *.dfm} Uses ed_izmer,product_group,postav_ik,products,prihod,ostatki,rashod; { **** UBPFD *********** by delphibase.endimus.com **** >> Программное сжатие базы данных Access используя JRO (Jet Replication Objects) Процедура позволяет сжать базу данных в формате Access, используя JRO (Jet Replication Objects). Действие аналогичное пункту меню в Access "Сервис -> Служебные программы -> Сжать и восстановить базу данных". Параметры: * DatabaseName - путь к исходной (не сжатой) базе данных * DestDatabaseName - путь к сжатой базе данных (по умолчанию пустой - в этом случае исходная база заменяется сжатой) * Password - пароль базы данных (по умолчанию пустой) Зависимости: windows,SysUtils,ComObj,Dialogs (Dialogs можно исключить используя MessageBox для вывода сообщения исключительной ситуации) Автор: savva, [email protected], ICQ:126578975, Орел Copyright: Сапронов Алексей (Savva) Дата: 9 сентября 2002 г. ***************************************************** } procedure TForm_general.CompactDatabase_JRO(DatabaseName: string; DestDatabaseName: string = ''; Password: string = ''); const Provider = 'Provider=Microsoft.Jet.OLEDB.4.0;'; var TempName: array[0..MAX_PATH] of Char; // имя временного файла TempPath: string; // путь до него Name: string; Src, Dest: WideString; V: Variant; begin try Src := Provider + 'Data Source=' + DatabaseName; if DestDatabaseName <> '' then Name := DestDatabaseName else begin // выходная база не указана - используем временный файл // получаем путь для временного файла TempPath := ExtractFilePath(DatabaseName); if TempPath = '' then TempPath := GetCurrentDir; //получаем имя временного файла GetTempFileName(PChar(TempPath), 'mdb', 0, TempName); Name := StrPas(TempName); end; DeleteFile(PChar(Name)); // этого файла не должно существовать, удаляем его :)) Dest := Provider + 'Data Source=' + Name; if Password <> '' then begin Src := Src + ';Jet OLEDB:Database Password=' + Password; Dest := Dest + ';Jet OLEDB:Database Password=' + Password; end; V := CreateOleObject('jro.JetEngine'); try V.CompactDatabase(Src, Dest); // сжимаем finally V := 0; end; if DestDatabaseName = '' then begin // т.к. выходная база не указана DeleteFile(PChar(DatabaseName)); //то удаляем не упакованную базу // и переименовываем упакованную базу if RenameFile(Name, DatabaseName) then begin ShowMessage('Процедура сжатию и восстановлению базы данных успешно завершена. Сейчас программа будет закрыта.'); Application.Terminate; end; end; except // выдаем сообщение об исключительной ситуации on E: Exception do ShowMessage(e.message); end; end; Сохраняем проект. Внимание, при запуске из Delphi данной процедуры будет выскакивать ошибка: Поэтому прежде, чем запустить проект его необходимо скомпилировать, и закрыть Delphi, а только потом запускать приложение. 4. САМОСТОЯТЕЛЬНАЯ РАБОТА СТУДЕНТА СРО №1 Тема: Технологии создания баз данных в Delphi. Цель:Рассмотреть технологии создания базы данных в Delphi. Рассматриваемые вопросы: - Технология BorlandDatabaseEngine (BDE); - Технология ActiveXDataObject (ADO); - Сравнение BDE и ADO; - Архитектура приложения баз данных. СРО №2 Тема: Технология ADO. Цель: Рассмотреть технологию ADО. Рассматриваемые вопросы: - Основа архитектуры ADО; - Интерфейсы ADО; - Компоненты расположенные на закладке ADO. СРО №3 Тема: Наборы данных. Основные свойства, методы и события. Цель:Рассмотреть основные свойства, методы и события наборов данных. Рассматриваемые вопросы: - Класс TDataSet; - Основные механизмы, реализованные в наборе данных; - Основные свойства, методы и события наборов данных. СРО №4 Тема: Создание базы данных «Моя группа». Цель:Используя приобретенные навыки работы с технологией ADО в Delphi создать базу данных. Содержание задания: Приложение базы данныз разработанный в среде Delphi.