Диалоговое окно New Items с выбранным объектом типа "поток"
Рисунок 29.3. Диалоговое окно New Items с выбранным объектом типа "поток"
Диалоговое окно New Thread Object
Рисунок 29.4. Диалоговое окно New Thread Object
5. Когда появится диалоговое окно для именования объекта поток, введите TPiThread и нажмите клавишу <Enter> (Рисунок 29.4). Помимо этого, при желании, вы можете присвоить создаваемому потоку имя, установив флажок Named Thread и задав имя в поле Thread Name. Так как имя потока используется только для удобства обозначения, эту возможность мы использовать не будем.
Delphi создаст новый модуль и поместит в него шаблон для нового потока.
6. Код, вносимый в метод Execute, вычисляет число я, используя сходимость бесконечного ряда Лейбница:
Pi = 4 - 4/3 + 4/5 - 4/7 + 4/9 -...
Разумеется, отображать новое значение после каждой итерации — это то же самое, что стрелять из пушки по воробьям. На отображение информации система потратит в десятки раз больше времени, чем на собственно вычисления. Поэтому мы ввели константу updatePeriod, которая регулирует периодичность отображения текущего значения.
Код метода Execute показан ниже:
const
// Лучше использовать нечетное число для того, чтобы избежать эффекта // мерцания UpdatePeriod = 1000001;
procedure TPiThread.Execute; var sign : Integer;
PiValue, PrevValue : Extended; i : Int64;
begin
{ Place thread code here } PiValue := 4; sign := -1; i := 0; repeat Inc(i);
PrevValue := PiValue;
PiValue := PiValue + sign * 4 / (2*i+l); sign := -sign;
if i mod UpdatePeriod = 0 then
begin
GlobalPi := PiValue; GlobalCounter := i; Synchronize(fmMain.UpdatePi);
end;
until Terminated or (Abs(PiValue - PrevValue)<1E-19); end;
7. Откройте меню File и выберите пункт Save As. Сохраните модуль с потоком как uPiThread.pas.
8. Отредактируйте главный файл модуля uMain.pas и добавьте модуль uPiThread к списку используемых модулей в секции интерфейса. Он должен выглядеть так:
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, StdCtrls, uPiThread;
9. В секции public формы TfmMain добавьте ссылку на создаваемую нить: PiThread : TPiThread;
10. Добавьте в модуль uMain две глобальные переменные
GlobalPi : Extended;
GlobalCounter : Int64;
и метод UpdatePi:
procedure TfmMain.UpdatePi;
begin
if Islconic(Application.Handle) then
Exit;
LaValue.Caption := FloatToStrF(GlobalPi, ffFixed, 18, 18);
lalterNum.Caption := IntToStr(GlobalCounter) + ' iterations';
end;
Этот метод, если вы обратили внимание, вызывается из потока посредством процедуры Synchronize. Он отображает текущее значение приближения к числу "пи" и количество итераций.
В случае, если главное окно приложения свернуто, отображение не производится; так что после его развертывания вам, возможно, придется подождать некоторое время для обновления.
11. Выполните двойной щелчок на свободном месте рабочей области формы, при этом создастся шаблон метода FormCreate. Здесь мы отобразим значение системной константы р±:
procedure TfmMain.FormCreate(Sender: TObject);
begin
laBuiltln.Caption := FloatToStrF(Pi, ffFixed, 18, 18); end;
12. Выберите на форме переключатель (его название cbcalcuiate) и назначьте событию Onclick код, создающий и уничтожающий вычислительный поток в зависимости от состояния переключателя:
procedure TfmMain.cbCalculateClick(Sender: TObject);
begin if cbCalculate.Checked then
begin
PiThread := TPiThread.Create(True);
PiThread.FreeOnTerminate := True;
PiThread.Priority := tpLower;
PiThread.Resume; end else begin
if Assigned(PiThread) then PiThread.Terminate;
end;
end;
Таким образом, многопоточное приложение готово к запуску. Если все пройдет нормально, вы увидите картинку, подобную той, которая приведена на Рисунок 29.5.
Фоновые процедуры, или способ обойтись без потоков
Фоновые процедуры, или способ обойтись без потоков
Здесь мы рассмотрим возможность для организации фоновых действий (job) внутри однопоточной программы с сохранением реакции этого потока на события от мыши и клавиатуры.
Еще не столь давно программисты пытались эмулировать потоки, запуская процедуры внутри цикла обработки сообщений Windows. Цикл обработки сообщений (или цикл ожидания) — это особый фрагмент кода в программе, управляемой событиями. Он исполняется тогда, когда программа находит в очереди события, которые нужно обработать; если таковых нет, программа может выполнить в это время "фоновую процедуру". Такой способ имитации потоков весьма сложен, т. к. вынуждает программиста, во-первых, сохранять состояние фоновой процедуры между ее вызовами, а во-вторых, определять момент, когда она вернет управление обработчику событий. Если такая процедура выполняется долго, то у пользователя может сложиться впечатление, что приложение перестало реагировать на внешние события. Использование потоков снимает проблему переключения контекста, теперь контекст (стек и регистры) сохраняет операционная система.
В Delphi возможность создать фоновую процедуру реализована через событие Onldle .объекта Application!
type TIdleEvent = procedure (Sender: TObject;
var Done: Boolean)
of object;
property Onldle: TIdleEvent;
Обработчик этого события вы можете написать, поместив на форму компонент TApplicationEvents со страницы Additional Палитры компонентов.
Чтобы сделать в фоновом режиме какую-то работу, следует разбить ее на кванты и выполнять по одному кванту каждый вызов Onldle — иначе приложение будет плохо реагировать на внешние воздействия.
Компоненты Rave Reports и отчеты в приложении Delphi
ГЛАВА 29
Потоки и процессы
Работая с Delphi, нужно иметь в виду: этот замечательный продукт не только упрощает разработку сложных приложений, он использует при этом все возможности операционной системы. Одна из возможностей, которую поддерживает Delphi, — это так называемые потоки (threads) или нити.
Потоки позволяют в рамках одной программы решать несколько задач одновременно. С недавних пор операционные системы для персональных компьютеров сделали это возможным.
Операционная система (ОС) предоставляет приложению некоторый интервал времени центрального процессора (ЦП) и в момент, когда приложение переходит к ожиданию сообщений или освобождает процессор, операционная система передает управление другой задаче. Теперь, когда компьютеры с более чем одним процессором резко упали в цене, а операционная система Windows NT может использовать наличие нескольких процессоров, пользователи действительно могут запускать одновременно более одной задачи. Планируя время центрального процессора, Windows 95 или Windows NT распределяют его между потоками, а не между приложениями. Чтобы использовать все преимущества, обеспечиваемые несколькими процессорами в современных операционных системах, программист должен знать, как создавать потоки.
В этой главе рассматриваются следующие вопросы:
что такое потоки; разница между потоком и процессом; преимущества потоков; класс TThread в Delphi; реализация многопоточного приложения; синхронизация потоков.Гонки
Гонки
Ситуация гонок возникает, когда два или более потока пытаются получить доступ к общему ресурсу и изменить его состояние. Рассмотрим следующий пример. Пусть Поток 1 получил доступ к ресурсу и изменил его в своих интересах; затем активизировался Поток 2 и модифицировал этот же ресурс до завершения Потока 1. Поток 1 полагает, что ресурс остался в том же состоянии, в каком был до переключения. В зависимости от того, когда именно был изменен ресурс, результаты могут варьироваться — иногда код будет выполняться нормально, иногда нет. Программисты не должны строить никаких гипотез относительно порядка исполнения потоков, т. к. планировщик ОС может запускать и останавливать их в любое время.
Inc(i) ;
if i = iSomething then DoSomething;
Здесь i — глобальная переменная, доступная из обоих потоков. Пусть два или более потоков исполняют этот код одновременно. Поток 1 инкрементировал значение переменной i и хочет проверить ее значение для выполнения тех или иных условий. Но тут активизируется другой поток, который еще увеличивает значение i. В результате первый поток "проскакивает" мимо условия, которое, казалось бы, должно было быть выполнено.
Возникновения как ситуаций гонок, так и тупиков можно избежать, если использовать приемы, обсуждаемые ниже.
Как избежать одновременного запуска двух копий одного приложения
Как избежать одновременного запуска двух копий одного приложения
Такая задача возникает очень часто. Многие, особенно начинающие, пользователи не вполне понимают, что между щелчком по значку приложения и его запуском может пройти несколько секунд, а то и десятков секунд. Они начинают щелкать по значку, запуская все новые копии. Между тем, при работе с базами данных и во многих других случаях иметь более одной копии не только не нужно, но и вредно.
Идея заключается в том, чтобы первая создаваемая копия приложения захватывала некий, ресурс, а все последующие при запуске пытались сделать то же самое и в случае неудачи завершались.
Пример такого ресурса — общий блок в файле, отображаемом в память. Поскольку этот ресурс имеет имя, можно сделать его уникальным именно для вашего приложения:
var UniqueMapping : THandle;
FirstWindow : THandle
; begin
UniqueMapping := CreateFileMapping($ffffffff,
nil, PAGE_READONLY, 0, 32,'MyMap');
if UniqueMapping = 0 then
begin
ShowMessage(SysErrorMessage(GetLastError));
Halt;
end
else if GetLastError = ERROR_ALREADY_EXISTS then
begin
FirstWindow := FindWindowEx(0, 0, TfmMain.ClassName, nil);
if FirstWindowoO then
SetForegroundWindow(FirstWindow};
Halt;
end;
// Нет других копий — продолжение Application.Initialize;
Примерно такие строки нужно вставить в начало текста проекта до создания форм. Блок совместно используемой памяти выделяется в системном страничном файле (об этом говорит первый параметр, равный -1, см. описание функции CreateFileMapping). Его имя — муМар. Если при создании блока будет получен код ошибки ERROR_ALREADY__EXISTS, это свидетельствует о наличии работающей копии приложения. В этом случае приложение переключает фокус на главную форму другого экземпляра и завершается; в противном случае процесс инициализации продолжается.
Класс TThread
Класс TThread
Delphi представляет программисту полный доступ к возможностям программирования интерфейса Win32. Для чего же тогда фирма Borland представила специальный класс для организации потоков? Вообще говоря, программист не обязан разбираться во всех тонкостях механизмов, предлагаемых операционной системой. Класс должен инкапсулировать и упрощать программный интерфейс; класс TThread — прекрасный пример предоставления разработчику простого доступа к программированию потоков. Сам API потоков, вообще говоря, не очень сложен, но предоставленные классом TThread возможности вообще замечательно просты. В двух словах, все, что вам необходимо сделать, — это перекрыть виртуальный метод Execute.
Другая отличительная черта класса TThread — это гарантия безопасной работы с библиотекой визуальных компонентов VCL. Без использования класса TThread во время вызовов VCL могут возникнуть ситуации, требующие специальной синхронизации (см. разд. "Проблемы при синхронизации потоков" далее в этой главе).
Нужно отдавать себе отчет, что с точки зрения операционной системы поток — это ее объект. При создании он получает дескриптор и отслеживается ОС. Объект класса TThread — это конструкция Delphi, соответствующая потоку ОС. Этот объект VCL создается до реального возникновения потока в системе и уничтожается после его исчезновения.
Изучение класса TThread начнем с метода Execute:
procedure Execute; virtual; abstract;
Это и есть код, исполняемый в создаваемом вами потоке TThread.
Примечание
Примечание
Хотя формальное описание Execute — метод abstract, но мастер создания нового объекта TThread создает для вас пустой шаблон этого метода.
Переопределяя метод Execute, мы можем тем самым закладывать в новый потоковый класс то, что будет выполняться при его запуске. Если поток был создан с аргументом CreateSuspended, равным False, то метод Execute выполняется немедленно, в противном случае Execute выполняется после вызова метода Resume (см. описание конструктора ниже).
Если поток рассчитан на однократное выполнение каких-либо действий, то никакого специального кода завершения внутри Execute писать не надо.
Если же в потоке будет выполняться какой-то цикл, и поток должен завершиться вместе с приложением, то условия окончания цикла должны быть примерно такими:
procedure TMyThread.Execute;
begin
repeat
DoSomething;
Until CancelCondition or Terminated;
end;
Здесь CancelCondition — ваше личное условие завершения потока (исчерпание данных, окончание вычислений, поступление на вход того или иного символа и т. п.), а свойство Terminated сообщает о завершении потока (это свойство может быть установлено как изнутри потока, так и извне; скорее всего, завершается породивший его процесс).
Конструктор объекта:
constructor Create(CreateSuspended: Boolean);
получает параметр CreateSuspended. Если его значение равно True, вновь созданный поток не начинает выполняться до тех пор, пока не будет сделан вызов метода Resume. В случае, если параметр CreateSuspended имеет значение False, конструктор завершается и только затем поток начинает исполнение.
destructor Destroy; override;
Деструктор Destroy вызывается, когда необходимость в созданном потоке отпадает. Деструктор завершает его и высвобождает все ресурсы, связанные с объектом TThread. function Terminate: Integer;
Для окончательного завершения потока (без последующего запуска) существует метод Terminate. Но если вы думаете, что этот метод делает какие-то принудительные действия по остановке потока, вы ошибаетесь. Все, что происходит, — это установка свойства
property Terminated: Boolean;
в значение True. Таким образом, Terminate — это указание потоку завершиться, выраженное "в мягкой форме", с возможностью корректно освободить ресурсы. Если вам нужно немедленно завершить поток, используйте функцию Windows API TerminateThread.
Примечание
Примечание
Метод Terminate автоматически вызывается и из деструктора объекта. Поток— объект VCL будет дожидаться, пока завершится поток— объект операционной системы. Таким образом, если поток не умеет завершаться корректно, вызов деструктора потенциально может привести к зависанию всей программы.
Еще одно полезное свойство:
property FreeOnTerminate: Boolean;
Если это свойство равно True, то деструктор потока будет вызван автоматически по его завершении. Это очень удобно для тех случаев, когда вы в своей программе не уверены точно, когда именно завершится поток, и хотите использовать его по принципу "выстрелил и забыл" (fire and forget).
function WaitFor: Integer;
Метод WaitFor предназначен для синхронизации и позволяет одному потоку дождаться момента, когда завершится другой поток. Если вы внутри потока FirstThread пишите код
Code := SecondThread.WaitFor;
то это означает, что поток FirstThread останавливается до момента завершения потока SecondThread. Метод WaitFor возвращает код завершения ожидаемого потока (см. свойство Returnvalue).
property Handle: THandle read FHandle;
property ThreadID: THandle read FThreadID;
Свойства Handle и ThreadID дают программисту непосредственный доступ к потоку средствами API Win32. Если разработчик хочет обратиться к потоку и управлять им, минуя возможности класса TThread, значения Handle и ThreadID могут быть использованы в качестве аргументов функций Win32 API. Например, если программист хочет перед продолжением выполнения приложения дождаться завершения сразу нескольких потоков, он должен вызвать функцию API waitForMuitipieObjects; для ее вызова необходим массив дескрипторов потоков.
property Priority: TThreadPriority;
Свойство Priority позволяет запросить и установить приоритет потоков. Приоритеты потоков в деталях описаны выше. Допустимыми значениями приоритета для объектов TThread являются tpidle, tpLowest, tpLower, tpNormai, tpHigher, tpHighest и tpTimeCritical.
procedure Synchronize(Method: TThreadMethod);
Этот метод относится к секции protected, т. е. может быть вызван только из потомков TThread. Delphi предоставляет программисту метод Synchronize для
безопасного вызова методов VCL внутри потоков. Во избежание конфликтных ситуаций, метод synchronize дает гарантию, что к каждому объекту VCL одновременно имеет доступ только один поток. Аргумент, передаваемый в метод Synchronize, — это имя метода, который производит обращение к VCL; вызов Synchronize с этим параметром — это то же, что и вызов самого метода. Такой метод (класса TThreadMethod) не должен иметь никаких параметров и не должен возвращать никаких значений. К примеру, в основной форме приложения нужно предусмотреть функцию
procedure TMainForm.SyncShowMessage; begin
ShowMessagedntToStr (ThreadListl. Count) ) ; // другие обращения к VCL
end;
а в потоке для показа сообщения писать не
ShowMessage(IntToStr(ThreadListl.Count));
и даже не
MainForm.SyncShowMessage;
а только так:
Synchronize(MainForm.SyncShowMessage);
Примечание
Примечание
Производя любое обращение к объекту VCL из потока, убедитесь, что при этом используется метод Synchronize; в противном случае результаты могут оказаться непредсказуемыми. Это верно даже в том случае, если вы используете средства синхронизации, описанные ниже.
procedure Resume;
Метод Resume класса TThread вызывается, когда поток возобновляет выполнение после остановки, или для явного запуска потока, созданного с параметром CreateSuspended, равным True.
procedure Suspend;
Вызов метода Suspend приостанавливает поток с возможностью повторного запуска впоследствии. Метод suspend приостанавливает поток вне зависимости от кода, исполняемого потоком в данный момент; выполнение продолжается с точки останова.
property Suspended: Boolean;
Свойство suspended позволяет программисту определить, не приостановлен ли поток. С помощью этого свойства можно также запускать и останавливать поток. Установив свойство suspended в значение True, вы получите тот же результат, что и при вызове метода Suspend — приостановку. Наоборот, установка свойства Suspended в значение False возобновляет выполнение потока, как и вызов метода Resume.
property ReturnValue: Integer;
Свойство ReturnValue позволяет узнать и установить значение, возвращаемое потоком по его завершении. Эта величина полностью определяется пользователем. По умолчанию поток возвращает ноль, но если программист захочет вернуть другую величину, то простая переустановка свойства ReturnValue внутри потока позволит получить эту информацию другим потокам. Это, к примеру, может пригодиться, если внутри потока возникли проблемы, или с помощью свойства ReturnValue нужно вернуть число не прошедших орфографическую проверку слов.
На этом завершим подробный обзор класса TThread. Для более близкого знакомства с потоками и классом Delphi TThread создадим многопоточное приложение. Для этого нужно написать всего несколько строк кода и несколько раз щелкнуть мышью.
Консольный ввод
Консольный ввод
Консольный ввод (console input) годится для потоков, которые должны ожидать отклика на нажатие пользователем клавиши на клавиатуре. Этот тип ожидания может быть использован в программе дуплексной связи (chat). Один поток при этом будет ожидать получения символов; второй — отслеживать ввод пользователя и затем отсылать набранный текст ожидающему приложению.
Критическая секция
Критическая секция
Работая в Delphi, программист может также использовать объект типа критическая секция (critical section). Критические секции подобны взаимным исключениям по сути, однако между ними существуют два главных отличия:
взаимные исключения могут быть совместно использованы потоками в различных процессах, а критические секции — нет; если критическая секция принадлежит другому потоку, ожидающий поток блокируется вплоть до освобождения критической секции. В отличие от этого, взаимное исключение разрешает продолжение по истечении тайм-аута.Критические секции и взаимные исключения очень схожи. На первый взгляд, выигрыш от использования критической секции вместо взаимного исключения не очевиден. Критические секции, однако, более эффективны, чем взаимные исключения, т. к. используют меньше системных ресурсов. Взаимные исключения могут быть установлены на определенный интервал времени, по истечении которого выполнение продолжается; критическая секция всегда ждет столько, сколько потребуется.
Возьмем класс TCriticalSection (модуль SYNCOBJS.PAS). Логика использования его проста — "держать и не пущать". В многопотоковом приложении создается и инициализируется общая для всех потоков критическая секция. Когда один из потоков достигает критически важного участка кода, он пытается захватить секцию вызовом метода Enter:
MySection.Enter; try DoSomethingCritical;
finally
MySection.Leave;
end;
Когда другие потоки доходят до оператора захвата секции Enter и обнаруживают, что она уже захвачена, они приостанавливаются вплоть до освобождения секции первым потоком путем вызова метода Leave. Обратите внимание, что вызов Leave помещен в конструкцию try. .finally — здесь требуется стопроцентная надежность. Критические секции являются системными объектами и подлежат обязательному освобождению — впрочем, как и остальные рассматриваемые здесь объекты.
Локальные данные потока
Локальные данные потока
Интересная проблема возникает, если в приложении будет несколько одинаковых потоков. Как избежать совместного использования одних и тех же переменных несколькими потоками? Первое, что приходит на ум, — добавить и использовать поля объекта — потомка TThread, которые можно добавить при его создании. Каждый поток соответствует отдельному экземпляру объекта, и их данные пересекаться не будут. (Кстати, это одно из больших удобств использования класса TThread.) Но есть функции API, которые знать не знают об объектах Delphi и их полях и свойствах. Для поддержки разделения данных между потоками на нижнем уровне в язык Object Pascal введена специальная директива — threadvar, которая отличается от директивы описания переменных var тем, что применяется только к локальным данным потока. Следующее описание:
Var
datal: Integer; threadvar
data2: Integer;
означает, что переменная datal будет использоваться всеми потоками данного приложения, а переменная data2 будет у каждого потока своя.
Обзор потоков
Обзор потоков
Определение потока довольно простое: потоки — это объекты, получающие время процессора. Время процессора выделяется квантами (quantum, time slice). Квант времени — это интервал, имеющийся в распоряжении потока до тех пор. пока время не будет передано в распоряжение другого потока.
Обратите внимание, что кванты выделяются не программам или процессам, а порожденным ими потокам. Как минимум, каждый процесс имеет хотя бы один (главный) поток, но современные операционные системы, начиная с Windows 95 (для приверженцев Borland Kylix и Linux также), позволяют запустить в рамках процесса несколько потоков.
Если вы новичок в использовании потоков, самый простой пример их использования — приложения из состава Microsoft Office. К примеру, пакеты Excel и Word задействуют по несколько потоков. Word может одновременно корректировать грамматику и печатать, при этом осуществляя ввод данных с клавиатуры и мыши; программа Excel способна выполнять фоновые вычисления и печатать.
Примечание
Примечание
Узнать число потоков, запущенных приложением, в Windows NT, 2000 и ХР можно при помощи утилиты Task Manager (Диспетчер задач). Для этого среди показателей, отображаемых в окне Processes, нужно выбрать опцию Thread Count. Так, в момент написания этих строк MS Word использовал 5 потоков, среда Delphi — 3.
Вполне возможно, что эту главу сейчас вы читаете из чистого любопытства. Но, более вероятно, вы пришли в поиске ответов на конкретные проблемы. Какого же рода проблемы могут быть решены с применением потоков?
Если задачи приложения можно разделить на различные подмножества: обработка событий, ввод/вывод, связь и др., то потоки могут быть органично встроены в программное решение. Если разработчик может разделить большую задачу на несколько мелких, это только повысит переносимость кода и возможности его многократного использования.
Сделав приложение многопоточным, программист получает дополнительные возможности управления им. Например, через управление приоритетами потоков. Если один из них "притормаживает" приложение, занимая слишком много процессорного времени, его приоритет может быть понижен.
Другое важное преимущество внедрения потоков — при возрастании "нагрузки" на приложение можно увеличить количество потоков и тем самым снять проблему.
Потоки упрощают жизнь тем программистам, которые разрабатывают приложения в архитектуре клиент/сервер. Когда требуется обслуживание нового клиента, сервер может запустить специально для этого отдельный поток. Такие потоки принято называть симметричными потоками (symmetric threads) — они имеют одинаковое предназначение, исполняют один и тот же код и могут разделять одни и те же ресурсы. Более того, приложения, рассчитанные на серьезную нагрузку, могут поддерживать пул (pool) однотипных потоков. Поскольку создание потока требует определенного времени, для ускорения работы желательно заранее иметь нужное число готовых потоков и активизировать их по мере подключения очередного клиента.
Примечание
Примечание
Такой подход особенно характерен для Web-сервера Microsoft Internet Information Services и приложений, обрабатывающих запросы в его среде. Если вы создаете приложения ISAPI на Delphi, то можете использовать пулинг потоков, подключив к проекту модуль ISAPIThreadPool.pas. Если вы хотите позаимствовать идеи для других целей, ознакомьтесь с содержимым этого модуля.
Асимметричные потоки (asymmetric threads) — это потоки, решающие различные задачи и, как правило, не разделяющие совместные ресурсы. Необходимость в асимметричных потоках возникает:
когда в программе необходимы длительные вычисления, при этом необходимо сохранить нормальную реакцию на ввод; когда нужно обрабатывать асинхронный ввод/вывод с использованием различных устройств (СОМ-порта, звуковой карты, принтера и т. п.); когда вы хотите создать несколько окон и одновременно обрабатывать ввод в них.Оповещение об изменении в файловой системе
Оповещение об изменении в файловой системе
Этот вид объекта ожидания очень интересен и незаслуженно мало известен. Мы рассмотрели практически все варианты того, как один поток может подать сигнал другому. А как получить сигнал от операционной системы? Ну, например, о том, что в файловой системе произошли какие-то изменения? Такой вид оповещения позаимствован из ОС UNIX и доступен программистам, работающим с Win32. Для организации мониторинга файловой системы нужно использовать
Три функции — FindFirstChangeNotification, FindNextChangeNotification и FinddoseChangeNotification. Первая из них возвращает дескриптор объекта файлового оповещения, который можно передать в функцию ожидания. Объект активизируется тогда, когда в заданной папке произошли те или иные изменения (создание или уничтожение файла или папки, изменение прав доступа и т. д.). Вторая — готовит объект к реакции на следующее изменение. Наконец, с помощью третьей функции следует закрыть ставший ненужным объект.
Так может выглядеть код метода Execute потока, созданного для мониторинга файловой системы:
var DirName : string;
...
procedure TSimpleThread.Execute;
var r: Cardinal;
fn : THandle;
begin
fn := FindFirstChangeNotification(pChar(DirName), True,
FILEJTOTIFY_CHANGE_FILE_NAME);
repeat
r := WaitForSingleObject(fn,2000);
if r = WAIT_OBOECT_0 then
Synchronize(Forml.UpdateList);
if not FindNextChangeNotification(fn) then
break;
until Terminated;
FindCloseChangeNotification(fn);
end;
На главной форме должны находиться компоненты, нужные для выбора обследуемой папки, а также компонент TListBox, в который будут записываться имена файлов:
procedure TForml.ButtonlClick(Sender: TObject);
var dir : string; begin
if SelectDirectory(dir,[],0)
then begin
Editl.Text := dir; DirName := dir;
end;
end;
procedure TForml.UpdateList;
var SearchRec: TSearchRec;
begin
ListBoxl.Clear;
FindFirst(Editl.Text+'\*.*', faAnyFile, SearchRec); repeat ListBoxl.Items.Add(SearchRec.Name);
until FindNext(SearchRec) <> 0;
FindClose(SearchRec);
end;
Приложение готово. Чтобы оно стало полнофункциональным, предусмотрите в нем механизм перезапуска потока при изменении обследуемой папки.
Поток
Поток
Поток может ожидать другой поток точно так же, как и другой процесс. Ожидание можно организовать с помощью функций API (как в только что рассмотренном примере), но удобнее это сделать при помощи метода TThread.WaitFor.
Потоки и процессы
Потоки и процессы
Когда мы говорим "программа" (application), то обычно имеем в виду понятие, в терминологии операционной системы обозначаемое как "процесс". Процесс состоит из виртуальной памяти, исполняемого кода, потоков и данных. Процесс может содержать много потоков, но обязательно содержит, по крайней мере, один. Поток, как правило, имеет "в собственности" минимум ресурсов; он зависит от процесса, который и распоряжается виртуальной памятью, кодом, данными, файлами и другими ресурсами ОС.
Почему мы используем потоки вместо процессов, хотя, при необходимости, приложение может состоять и из нескольких процессов? Дело в том, что переключение между процессами — значительно более трудоемкая операция, чем переключение между потоками. Другой довод в пользу использования потоков — то, что они специально задуманы для разделения ресурсов; разделить ресурсы между процессами (имеющими раздельное адресное пространство) не так-то просто.
Пример создания многопоточного приложения в Delphi
Пример создания многопоточного приложения в Delphi
Этот раздел содержит описание шагов, необходимых для создания простого, но показательного примера многопоточного приложения. Мы будем пытаться вычислить число "пи" с максимальной точностью после запятой. Конечно, встроенная в Delphi константа Pi имеет достаточную точность, правильнее сказать — максимальную, допускаемую самым точным 10-байтным форматом для вещественных чисел Extended. Так что превзойти ее нам не удастся. Но этот пример использования потоков может послужить прологом для решения реальных задач.
Первый пример будет содержать два потока: главный (обрабатывающий ввод пользователя) и вычислительный; мы сможем изменять их свойства и наблюдать за реакцией. Итак, выполните следующую последовательность действий:
1. В среде Delphi откройте меню File и выберите пункт New Application.
2. Расположите на форме пять меток и один переключатель, как показано на Рисунок 29.2.
Переименуйте главную форму в fmMain.
3. Откройте меню File и выберите пункт Save Project As. Сохраните модуль как uMain, а проект — как Threads 1.
Приоритеты потоков
Приоритеты потоков
Интерфейс Win32 API позволяет программисту управлять распределением времени между потоками; это распространяется и на приложения, написанные на Delphi. Операционная система планирует время процессора в соответствии с приоритетами потоков.
Приоритет потока — величина, складывающаяся из двух составных частей: приоритета породившего поток процесса и собственно приоритета потока. Когда поток создается, ему назначается приоритет, соответствующий приоритету породившего его процесса.
В свою очередь, процессы могут иметь следующие классы приоритетов.
Real time; Normal; High; Below normal; Above normal; Idle.
Примечание
Примечание
Классы Above normal и Below normal появились впервые в Windows 2000.
Класс реального времени задает приоритет даже больший, чем у многих процессов операционной системы. Такой приоритет нужен для процессов, обрабатывающих высокоскоростные потоки данных. Если такой процесс не завершится за короткое время, пользователь почувствует, что система перестала откликаться, т. к. даже обработка событий мыши не получит времени процессора.
Использование класса High ограничено процессами, которые должны завершаться за короткое время, чтобы не вызвать сбойной ситуации. Пример — процесс, который посылает сигналы внешнему устройству; причем устройство отключается, если не получит своевременный сигнал. Если у вас возникли проблемы с производительностью вашего приложения, было бы неправильно решать их просто за счет повышения его приоритета до high — такой процесс также влияет на всю ОС. Возможно, в этом случае следует модернизировать компьютер.
Большинство процессов запускается в рамках класса с нормальным приоритетом. Нормальный приоритет означает, что процесс не требует какого-либо специального внимания со стороны операционной системы.
И наконец, процессы с фоновым приоритетом запускаются лишь в том случае, если в очереди Диспетчера задач нет других процессов. Обычные виды приложений, использующие такой приоритет, — это программы сохранения экрана и системные агенты (system agents). Программисты могут использовать фоновые процессы для организации завершающих операций и реорганизации данных. Примерами могут служить сохранение документа или резервное копирование базы данных.
Приоритеты имеют значения от 0 до 31. Процесс, породивший поток, может впоследствии изменить его приоритет; в этой ситуации программист имеет возможность управлять скоростью отклика каждого потока.
Базовый приоритет нити складывается из двух составляющих, однако это не означает, что он просто равен их сумме. Взгляните на соответствующие величины, которые показаны в табл. 29.1. Для потока, имеющего собственный приоритет THREAD_PRIORITY_IDLE, базовый приоритет будет равен 1, невзирая на приоритет породившего его процесса.
И еще для класса Normal приведены по два приоритета, снабженные буквами В (Background) и F (Foreground). Объяснение этому дается ниже.
Проблемы при синхронизации потоков
Проблемы при синхронизации потоков
К сожалению, простота создания потоков подчас "компенсируется" сложностью их применения. Две типичные проблемы, с которыми программист может столкнуться при работе с потоками, — это тупики (deadlocks) и гонки (race conditions).
Процесс. Порождение дочернего процесса
Процесс. Порождение дочернего процесса
Объект типа процесс (process) может быть использован для того, чтобы приостановить выполнение потока в том случае, если он для своего продолжения нуждается в завершении процесса. С практической точки зрения такая проблема встает, когда нужно в рамках вашего приложения исполнить приложение, созданное кем-то другим, или, к примеру, сеанс MS-DOS.
Рассмотрим, как, собственно, один процесс может породить другой. Вместо устаревшей и поддерживаемой только для совместимости функции winExec, перекочевавшей из прежних версий Windows, гораздо правильнее использовать более мощную:
function CreateProcess(IpApplicationName: PChar; IpCorranandLine: PChar;
IpProcessAttributes, IpThreadAttributes: PSecurityAttributes;
blnheritHandles: BOOL;
dwCreationFlags: DWORD; IpEnvironment: Pointer;
IpCurrentDirectory: PChar;
const IpStartupInfo: TStartupInfo;
var IpProcessInformation: TProcessInformation): BOOL;
Первые два параметра ясны — это имя запускаемого приложения и передаваемые ему в командной строке параметры. Параметр dwCreationFlags содержит флаги, определяющие способ создания нового процесса и его будущий приоритет. Использованные в приведенном ниже листинге флаги означают: CREATE_NEW_CONSOLE —будет запущено новое консольное приложение с отдельным окном; NORMAL_PRIORITY_CLASS — нормальный приоритет.
Структура TStartupInfo содержит сведения о размере, цвете, положении окна создаваемого приложения. В нижеследующем примере (листинг 29.1) используется поле wshowwindow: установлен флаг SW_SHOWNORMAL, означающий визуализацию окна с нормальным размером.
На выходе функции заполняется структура IpProcessInformation. В ней программисту возвращаются дескрипторы и идентификаторы созданного процесса и его первичного потока. Нам понадобится дескриптор процесса — в нашем примере создается консольное приложение, затем происходит ожидание его завершения. "Просигналит" нам об этом именно объект IpProcessInformation.hProcess.
Листинг 29.1. Порождение дочернего процесса
var
IpStartupInfo: TStartupInfo;
IpProcessInformation: TProcessInformation;
begin
FillChar(IpStartupInfo,Sizeof(IpStartupInfo),10);
IpStartupInfo.cb := Sizeof(IpStartupInfo};
IpStartupInfo.dwFlags := STARTFJJSESHOWWINDOW; IpStartupInfo.wShowWindow := SW_SHOWNORMAL;
if not CreateProcess(nil,
PChar('ping localhost'),
nil,
nil,
false,
CREATE_NEW_CONSOLE or NORMAL_PRIORITY_CLASS,
nil,
nil,
IpStartupInfo, IpProcessInformation) then
ShowMessage(SysErrorMessage(GetLastError; )
else
begin
WaitForSingleObject
(IpProcessInformation.hProcess, 10000); CloseHandle(IpProcessInformation.hProcess);
end;
end;
Резюме
Потоки, как и другие мощные инструменты, должны быть использованы с осторожностью и без злоупотреблений, поскольку могут возникнуть ошибки, которые очень трудно найти. Есть очень много доводов за использование потоков, но есть и доводы против этого. Работа с потоками будет проще, если учитывать нижеприведенные положения.
Если потоки работают только с переменными, объявленными внутри их собственного класса, то ситуации гонок и тупиков крайне маловероятны. Другими словами, избегайте использования в потоках глобальных переменных и переменных других объектов.
Если вы обращаетесь к полям или методам объектов VCL, делайте это только посредством метода Synchronize. Не "пересинхронизируйте" ваше приложение, а не то оно будет работать как один единственный поток. Избыточно синхронизированное приложение теряет все преимущества от наличия нескольких потоков, т. к. они будут постоянно останавливаться и ждать синхронизации. Потоки предоставляют изящное решение некоторых сегодняшних проблем программирования; но они также усложняют и без того непростой процесс отладки. И все же преимущества потоков однозначно перевешивают их недостатки.
С помощью диалога Performance Options можно управлять алгоритмом назначения приоритетов
Рисунок 29.1. С помощью диалога Performance Options можно управлять алгоритмом назначения приоритетов
Теперь, разобравшись в приоритетах потоков, нужно обязательно сказать о том, как же их использует планировщик заданий для распределения процессорного времени.
Операционная система имеет различные очереди готовых к выполнению потоков — для каждого уровня приоритета свой. В момент распределения нового кванта времени она просматривает очереди — от высшего приоритета к низшему. Готовый к выполнению поток, стоящий первым в очереди, получает этот квант и перемещается в хвост очереди. Поток будет исполняться всю продолжительность кванта, если не произойдет одно из двух событий:
выполняющийся поток остановился для ожидания; появился готовый к выполнению поток с более высоким приоритетом.Теперь, наверное, вам более ясна опасность, исходящая от неоправданного завышения приоритетов. Ведь, если есть активные потоки с высоким приоритетом, ни один поток с более низким приоритетом ни разу не получит времени процессора. Эта проблема может подстерегать вас даже на уровне вашего приложения. Предположим, вы назначили вычислительному потоку приоритет THREAD_PRIORITY_ABOVE_NORMAL, а потоку, где обрабатывается ввод пользователя, — THREAD_PRIORITY_BELOW_NORMAL. Тогда вместо запланированного результата — совместить вычисления с нормальной реакцией приложения — вы получите строго обратный. Приложение вообще перестанет откликаться на ввод, и снять его будет возможно только с помощью средств ОС.
Так что нормальная практика для асимметричных потоков — это назначение потоку, обрабатывающему ввод, более высокого приоритета, а всем остальным — более низкого или даже приоритета idle, если этот поток должен выполняться только во время простоя системы.
Семафор
Семафор
Семафор (semaphore) подобен взаимному исключению. Разница между ними в том, что семафор может управлять количеством потоков, которые имеют к нему доступ. Семафор устанавливается на предельное число потоков, которым доступ разрешен. Когда это число достигнуто, последующие потоки будут приостановлены, пока один или более потоков не отсоединятся от семафора и не освободят доступ.
В качестве примера использования семафора рассмотрим случай, когда каждый из группы потоков работает с фрагментом совместно используемого пула памяти. Так как совместно используемая память допускает обращение к ней только определенного числа потоков, все прочие должны быть блокированы вплоть до момента, когда один или несколько пользователей пула откажутся от его совместного использования.
простейший выбор для задач синхронизации.
Событие
Объект типа событие (event) — простейший выбор для задач синхронизации. Он подобен дверному звонку — звенит до тех пор, пока его кнопка находится в нажатом состоянии, извещая об этом факте окружающих. Аналогично, и объект может быть в двух состояниях, а "слышать" его могут многие потоки сразу.
Класс TEvent (модуль SYNCOBJS.PAS) имеет два метода: setEvent и ResetEvent, которые переводят объект в активное и пассивное состояние соответственно. Конструктор имеет следующий вид:
constructor Create(EventAttributes: PSecurityAttributes;
ManualReset, InitialState: Boolean; const Name: string);
Здесь параметр initialstate — начальное состояние объекта, ManualReset — способ его сброса (перевода в пассивное состояние). Если этот параметр равен True, событие должно быть сброшено вручную. В противном случае событие сбрасывается по мере того, как стартует хоть один поток, ждавший данный объект.
На третьем методе:
TWaitResult = (wrSignaled, wrTimeout, wrAbandoned, wrError);
function WaitFor(Timeout: DWORD): TWaitResult;
остановимся подробнее. Он дает возможность ожидать активизации события в течение Timeout миллисекунд. Как вы могли догадаться, внутри этого метода происходит вызов функции waitFotsingieObject. Типичных результатов на выходе waitFor два — wrsignaied, если произошла активизация события, и wrTimeout, если за время тайм-аута ничего не произошло.
Примечание
Примечание
Если нужно (и допустимо!) ждать бесконечно долго, следует установить параметр Timeout в значение INFINITE.
Рассмотрим маленький пример. Включим в состав нового проекта объект типа TThread, наполнив его метод Execute следующим содержимым:
Var res: TWaitResult;
procedure TSimpleThread.Execute;
begin
e := TEvent.Create(nil,True,false, 'test');
repeat
e.ReSetEvent;
res := e.WaitFor(10000);
Synchronize(Showlnfo);
until Terminated; e.Free;
end;
procedure TSimpleThread.Showlnfo;
begin
ShowMessage(IntToStr(Integer (res)));
end;
На главной форме разместим две кнопки — нажатие одной из них запускает поток, нажатие второй активизирует событие:
procedure TForml.ButtonlClick(Sender: TObject);
begin
TSimpleThread.Create(False);
end;
procedure TForml.Button2Click(Sender: TObject);
begin
e.SetEvent;
end;
Нажмем первую кнопку. Тогда появившийся на экране результат (метод Showlnfo) будет зависеть от того, была ли нажата вторая кнопка или истекли отведенные 10 секунд.
События используются не только для работы с потоками — некоторые процедуры операционной системы автоматически переключают их. К числу
таких процедур относятся отложенный (overlapped) ввод/вывод и события, связанные с коммуникационными портами.
Средства синхронизации потоков
Средства синхронизации потоков
Проще всего говорить о синхронизации, если создаваемый поток не взаимодействует с ресурсами других потоков и не обращается к VCL. Допустим, у вас на компьютере несколько процессоров, и вы хотите "распараллелить" вычисления. Тогда вполне уместен следующий код:
MyCompThread := TComputationThread.Create(False);
// Здесь можно что-нибудь делать, пока второй поток производит вычисления
DoSomeWork;
// Теперь ожидаем его завершения
MyCompThread.WaitFor;
Приведенная схема совершенно недопустима, если во время своей работы поток MyCompThread обращается к VCL посредством метода synchronize. В этом случае поток ждет главный поток для обращения к VCL, а тот, в свою очередь, его — классический тупик.
За "спасением" следует обратиться к программному интерфейсу Win32. Он предоставляет богатый набор инструментов, которые могут понадобиться для организации совместной работы потоков.
Главные понятия для понимания механизмов синхронизации — функции ожидания и объекты синхронизации. В Windows API предусмотрен ряд функций, позволяющих приостановить выполнение вызвавшего эту функцию потока вплоть до того момента, как будет изменено состояние какого-то объекта, называемого объектом синхронизации (под этим термином здесь понимается не объект Delphi, а объект операционной системы). Простейшая из этих функций — waitForSingieCbject — предназначена для ожидания одного объекта.
К возможным вариантам относятся четыре объекта, которые разработаны специально для синхронизации: событие (event), взаимное исключение (mutex), семафор (semaphore) и таймер (timer).
Но кроме специальных объектов можно организовать ожидание и других объектов, дескриптор которых используется в основном для иных целей, но может применяться и для ожидания. К ним относятся: процесс (process), поток (thread), оповещение об изменении в файловой системе (change notification) и консольный ввод (console input).
Косвенно к этой группе может быть добавлена критическая секция (critical section).
Примечание
Примечание
Перечисленные выше средства синхронизации в основном инкапсулированы в состав классов Delphi. У программиста есть две альтернативы. С одной стороны, в состав библиотеки VCL включен модуль SYNCOBJS.PAS, содержащий классы для события (TEvent) и критической секции (TCriticalSection). С другой, с Delphi поставляется отличный пример IPCDEMOS, который иллюстрирует проблемы взаимодействия процессов и содержит модуль IPCTHRD.PAS с аналогичными классами — для того же события, взаимного исключения (TMutex), а также совместно используемой памяти (TSharedMem).
Перейдем к подробному описанию объектов, используемых для синхронизации.
Классы процессов и приоритеты их потоков (для Windows 2000 и ХР)
Таблица 29.1. Классы процессов и приоритеты их потоков (для Windows 2000 и ХР)
IDLE_ PRIORITY CLASS |
BELOW_ NORMAL PRIORITY CLASS |
NORMAL_ PRIORITY_ CLASS |
ABOVE_ NORMAL_ PRIORITY_ CLASS |
HIGH PRIORITY CLASS |
REALTIME PRIORITY CLASS |
|
THREAD_ PRIORITY_ IDLE |
1 |
1 |
1 |
1 |
1 |
16 |
THREAD_ PRIORITY LOWEST |
2 |
4 |
5(B) 7(F) |
8 |
11 |
22 |
THREAD_ PRIORITY_ BELOW NORMAL |
3 |
5 |
6(B) 8(F) |
9 |
12 |
23 |
THREAD_ PRIORITY_ NORMAL |
4 |
6 |
7(B) 9(F) |
10 |
13 |
24 |
THREAD PRIORITY_ ABOVE_ NORMAL |
5 |
7 |
8(В) 10(F) |
11 |
14 |
25 |
THREAD_ PRIORITY_ HIGHEST |
6 |
8 |
9(B) 11(F) |
12 |
15 |
26 |
THREAD_ PRIORITY TIME CRITICAL |
15 |
15 |
15 |
15 |
15 |
31 |
Помимо базового приоритета, описываемого в этой таблице, планировщик заданий (scheduler) может назначать так называемые динамические приоритеты. Для процессов класса NORMAL_PRIORITY_CLASS при переключении из фонового режима в режим переднего плана и в ряде других случаев приоритет потока, с которым создано окно переднего плана, повышается. Так работают все клиентские операционные системы от Microsoft. Серверные операционные системы оптимизированы для выполнения фоновых приложений. Впрочем, Windows NT и более поздние ОС на этом ядре позволяют переключать режим оптимизации, используя переключатель Application response апплета System панели управления Windows (Рисунок 29.1).
К тому же Windows 2000 Professional и Windows 2000 Server имеют разные алгоритмы выделения квантов времени. Первая — клиентская — операционная система выделяет время короткими квантами переменной длины для ускорения реакции на приложения переднего плана (foreground). Для сервера же более важна стабильная работа системных служб, поэтому во второй ОС система распределяет длинные кванты постоянной длины.
Тупики
Тупики
Вероятно, вы не раз наблюдали на трамвайной остановке следующую забавную картину (Рисунок 29.6).
Рис 29.6. Ситуации тупиков возникают не только в программировании
Рисунок дает исчерпывающее пояснение ситуации тупиков. Тупики имеют место, когда поток ожидает ресурс, который в данный момент принадлежит другому потоку. Рассмотрим пример. Поток 1 захватывает ресурс А, и для того чтобы продолжать работу, ждет возможности захватить ресурс Б. В то же время Поток 2 захватывает ресурс Б и ждет возможности захватить ресурс А. Развитие этого сценария заблокирует оба потока; ни один из них не будет исполняться. Ресурсами могут выступать любые совместно используемые объекты системы — файлы, массивы в памяти, устройства ввода/вывода и т. п.
В ситуации на картинке три трамвая захватили по одному ресурсу (перекрестку) и пытаются захватить еще один, что, очевидно, невозможно без освобождения уже захваченных. В жизни ситуация разрешилась просто — самый молодой из водителей был вынужден отъехать. В информационных технологиях все бывает сложнее. Откройте любой документ, сопровождающий очередной пакет обновления к любой версии Windows. Очень часто там можно найти информацию об одной-двух исправленных ситуациях тупиков.
Внешний вид формы для приложения
Рисунок 29.2. Внешний вид формы для приложения Threads'1
4. Откройте меню File и выберите пункт New. Затем дважды щелкните на объекте типа поток (значок Thread Object). Откроется диалоговое окно New Items, показанное на Рисунок 29.3.
Пока один из авторов писал
Рисунок 29.5. Выполняющееся приложение Threads1
Пока один из авторов писал текст этого раздела, запущенное одновременно приложение Threadsl выполнило пять миллиардов итераций и приблизилось к встроенному значению Pi в десятом разряде. Интересно, насколько хватит терпения у вас?
Этот простой пример — первый шаг в усвоении того, как от базового класса rrhread можно порождать собственные классы. Из-за своей простоты он не лишен недостатков; более того — если бы вычислительных нитей было не одна, а более, кое-какие приемы были бы даже ошибочными. Но — об этом ниже.
Взаимные исключения
Взаимные исключения
Объект типа взаимное исключение (mutex) позволяет только одному потоку в данное время владеть им. Если продолжать аналогии, то этот объект можно сравнить с эстафетной палочкой.
Класс, инкапсулирующий взаимное исключение, — TMutex — находится в модуле IPCTHRD.PAS (пример IPCDEMOS). Конструктор:
constructor Create (const Name: string);
задает имя создаваемого объекта. Первоначально он не принадлежит никому. (Но функция API createMutex, вызываемая в нем, позволяет передать созданный объект тому потоку, в котором это произошло.) Далее метод
function Get(TimeOut: Integer): Boolean;
производит попытку в течение Timeout миллисекунд завладеть объектом (в этом случае результат равен True). Если объект более не нужен, следует вызвать метод
function Release: Boolean;
Программист может использовать взаимное исключение, чтобы избежать считывания и записи общей памяти несколькими потоками одновременно.
Компоненты Rave Reports и отчеты в приложении Delphi
ГЛАВА 30
Многомерное представление данных
Помимо стандартных компонентов отображения данных в VCL Delphi имеются дополнительные компоненты, которые позволяют представлять данные в виде кросстаба. При этом заставить работать кросстаб с двумя и более полями почти так же просто, как и обычный компонент TDBGrid. Эти компоненты расположены на странице Decision Cube Палитры компонентов.
Кросстабом называется такое табличное представление данных, которое имеет переменную структуру по горизонтали и вертикали. Причем обозначения столбцов по вертикали и строк по горизонтали соответствуют значениям полей набора данных. В ячейках кросстаба содержатся не данные, а суммарные значения для двух полей, которые пересекаются в этой ячейке.
В настоящей главе рассматриваются следующие вопросы:
для чего необходим кросстаб; особенности запросов SQL для многомерного представления; компоненты многомерного представления и их взаимосвязь.Главная форма проекта DemoMDCube
Рисунок 30.5. Главная форма проекта DemoMDCube
Компонент Decisioncubel выполняет всю работу по созданию многомерного представления набора данных компонента DecisionQueryl. Причем, практически все делается без вмешательства пользователя.
При настройке компонента был использован специализированный редактор свойства DecisionMap. В нем были заданы названия для размерностей и значения для расчета максимального размера используемой памяти. Все заданные значения соответствуют рекомендованным.
Для связывания набора данных с визуальными компонентами использован компонент DecisionSource1. На него замыкаются все три визуальных компонента многомерного представления, использованные в проекте.
Управление осуществляется компонентом DecisionPivotl. Основной визуальный компонент DecisionGrid1 представляет многомерный набор данных в табличном виде. В исходном состоянии по горизонтали расположены размерности номеров накладных и наименований, по вертикали размешаются размерности дат заказов и покупателей.
Дополнительного программного кода проект не имеет.
Компонент TDecisionCube
Компонент TDecisionCube
Компонент TDecisionCube осуществляет преобразование набора данных, который содержится в компоненте TDecisionQuery, к виду, доступному для отображения визуальными компонентами многомерного представления данных (табл. 30.2). Обычную таблицу набора данных компонент преобразует в многомерный кросстаб. Число размерностей создаваемого кросстаба зависит от числа полей данных набора данных. Значения в ячейках кросстаба зависят от типа агрегатной функции в запросе SQL.
Компонент TDecisionGraph
Компонент TDecisionGraph
Компонент TDecisionGraph создает график на основе многомерного представления набора данных. Конкретный вид графика (назначение горизонтальной и вертикальной осей) зависит от настроек компонентов TDecisionCube и TDecisionPivot. По умолчанию к оси абсцисс привязывается первая вертикальная размерность, к оси ординат — первая сумма. Первая горизонтальная размерность отображается в легенде графика.
Одним из предков компонента TDecisionGraph является класс TChart, от которого унаследованы все многочисленные свойства и методы для настройки графика.
Для подключения к графику набора данных используется свойство
property DecisionSource: TDecisionSource;
которое ссылается на экземпляр компонента источника данных.
Сразу после подключения автоматически строится график с осями, заданными по умолчанию.
Для управления графиком во время выполнения можно использовать компонент TDecisionPivot.
Компонент TDecisionGrid
Компонент TDecisionGrid
Компонент TDecisionGrid предназначен для многомерного представления данных в табличном виде. Параметры отображаемого набора данных (какие размерности будут видны при открытии, как группировать данные, как управлять размерностями) настраиваются при помощи других компонентов многомерного представления. В компоненте TDecisionGrid можно настроить только свойства самой сетки.
Для управления графиком во время выполнения можно использовать компонент TDecisionPivot.
В табл. 30.5 приведены основные свойства и методы компонента.
Компонент TDecisionPivot
Компонент TDecisionPivot
Компонент TDecisionPivot предоставляет пользователю средства управления размерностями многомерного представления данных. В стандартном состоянии компонент представляет собой панель, разделенную на три части (табл. 30.6). Каждая часть имеет собственный набор кнопок.
Левая часть имеет единственную кнопку, щелчок на которой позволяет сделать выбор суммирующего поля из развернувшегося списка. Элементы списка соответствуют вычисляемым полям с использованием агрегатных функций из запроса соответствующего компонента TDecisionQuery.
Выбор поля приводит к изменению значений в ячейках кросстаба. Например, выбор поля с функцией зим изменит значения в ячейках на суммы полей, поле с функцией COUNT произведет подсчет количества элементов в полях.
Средняя часть панели предназначена для размещения кнопок, соответствующих горизонтальным размерностям.
Правая часть панели используется для кнопок, соответствующих вертикальным размерностям.
Кнопка в нажатом состоянии показывает размерность. Одновременно отображается и общая сумма по размерности.
Размерности можно менять местами и перемещать с вертикали на горизонталь и обратно. Для этого можно выбрать команду Moved to Column Area из всплывающего меню кнопки. Во время выполнения можно использовать обычное перетаскивание кнопок при помощи мыши.
Всплывающее меню кнопки размерности имеет еще одну команду — Drilled In. По этой команде размерность переходит в режим детального просмотра по каждому значению поля. Конкретное значение можно выбрать из списка, который открывается при щелчке на кнопке. В этом случае суммы в ячейках кросстаба рассчитываются не по всей размерности, а только по выбранному значению.
Компонент TDecisionQuery
Компонент TDecisionQuery
Компонент доступа к данным TDecisionQuery предназначен для создания набора данных, который был бы пригоден для многомерного представления. Для создания набора данных используется запрос SQL. Требования к запросу приведены выше.
Этот компонент является прямым наследником компонента TQuery и не имеет собственных свойств и методов. Для создания запросов можно воспользоваться специализированным редактором, который автоматизирует этот процесс.
Компонент TDecisionSource
Компонент TDecisionSource
Компонент TDecisionSource предназначен для связывания визуальных компонентов многомерного представления с компонентом TDecisionCube (табл. 30.4). Кроме того, за счет возможности подключения к этому компоненту нескольких визуальных компонентов одновременно, при изменении состояния одного визуального компонента осуществляется синхронизация многомерного представления во всех остальных компонентах.
От набора данных в визуальные компоненты передаются данные, обратно транслируются команды пользователя по управлению многомерным представлением данных.
Отображение данных
Отображение данных
При работе с кросстабом пользователь имеет дело с двумя визуальными компонентами многомерного представления данных. Это компонент TDecisionGrid, который представляет данные в табличном виде, и компонент TDecisionGraph, который представляет данные в виде графика.
С компонентом TDecisionCube они связаны при помощи компонента TDecisionSource (см. выше).
Данные в визуальных компонентах появляются после открытия набора данных в соответствующем компоненте TDecisionQuery. Причем эти компоненты полностью работоспособны уже во время разработки приложения.
Подготовка набора данных
Подготовка набора данных
Компоненты многомерного представления данных работают со специально созданным и подготовленным набором данных. Эта работа выполняется специальным компонентом доступа к данным — TDecisionQuery. Его непосредственным предком является компонент TQuery.
Набор данных формируется при помощи запроса, который основан на стандартном синтаксисе SQL 92. Для обеспечения работы многомерного представления данных запрос должен удовлетворять ряду требований.
1. В тексте запроса должны присутствовать только те поля, которые разработчик хочет показать в компонентах многомерного представления данных.
2. Поля запроса должны быть сгруппированы при помощи оператора GROUP BY.
3. Запрос должен содержать агрегатные функции, которые определяют вид
информации, отображаемой в ячейках кросстаба.
Компонент TDecisionQuery должен только обеспечить выполнение запроса и создание набора данных, он не имеет никаких дополнительных свойств или методов. Поэтому для создания набора данных можно использовать и обычный компонент TQuery. Преимущество компонента TDecisionQuery состоит в том, что он имеет специализированный редактор для создания текста запроса (Рисунок 30.2). Он вызывается командой Decision Query Editor из всплывающего меню компонента или двойным щелчком на компоненте. Элементы управления страницы Dimensions/Summaries позволяют создавать текст запроса, манипулируя именами полей таблиц. Псевдоним базы данных выбирается в комбинированном списке Database. После этого в списке Table задается нужная таблица. Если в запросе требуется использовать несколько таблиц, то для их выбора можно воспользоваться утилитой SQL Builder, которая вызывается щелчком на одноименной кнопке.
Понятие кросстаба
Понятие кросстаба
Обычная таблица данных имеет строго заданное число столбцов, причем каждый столбец всегда предназначен для представления данных из одного поля. Для кросстаба число и назначение столбцов зависит от значений какого-либо поля. Число строк в кросстабе не равно числу строк в таблице БД, а также зависит от значений какого-либо поля. В ячейках кросстаба всегда располагается суммирующая информация по значениям полей горизонтали и вертикали (Рисунок 30.1).
Создать подобную двумерную структуру отображения данных при помощи обычных компонентов со страницы Data Controls Палитры компонентов очень непросто и хлопотно.
Пример кросстаба
Рисунок 30.1. Пример кросстаба
В общем случае горизонтальную и вертикальную структуры кросстаба могут составлять несколько полей одновременно, которые сгруппированы относительно более общих полей.
Для создания наборов данных, которые можно представить в виде кросстаба, используются запросы SQL с применением группирующего оператора GROUP BY и агрегатных функций. Если обратиться к топологическим аналогиям, то набор данных такого запроса представляет собой многомерный гиперкуб, каждая сторона которого соответствует одному полю набора данных.
Для дальнейшего изложения необходимо ввести еще одно понятие. Совокупность строк или колонок, имеющих отношение к одному полю набора данных, будем называть размерностью. Размерность представляет собой виртуальную плоскость, которая рассекает многомерный куб данных параллельно какой-либо стороне этого куба. Компоненты многомерного представления данных как раз предназначены для того, чтобы визуализировать это n-мерное сечение.
Переходя от пространственных моделей к наборам данных, можно сказать, что размерность представляет собой совокупность значений какого-либо поля в кросстабе относительно других полей.
Пример многомерного представления данных
Пример многомерного представления данных
В качестве примера использования многомерного представления данных рассмотрим демонстрационное приложение DemoMDCube (Рисунок 30.5).
В качестве исходного набора данных используется запрос к таблицам SALES и CUSTOMER общедоступной базы данных EMPLOYEE.GDB в составе поставки InterBase следующего вида:
SELECT С.CUSTOMER, S.ORDERJDATE, SUM(S.DISCOUNT) FROM SALES S
INNER JOIN CUSTOMER С
ON (C.CUST_NO = S.CUST_NO) GROUP BY C.CUSTOMER, S.ORDER_DATE
Запрос удовлетворяет всем требованиям для обеспечения многомерного представления данных. Для выполнения запроса и создания набора данных в проекте существует компонент DecisionQueryl.
Набор данных компонента открыт во время разработки, поэтому все компоненты многомерного представления в проекте ведут себя так же, как и во время выполнения программы.
С ним связан компонент Decisioncubel, для которого свойство имеет следующее значение:
DecisionCubel.DataSet := DecisionQueryl;
Многомерное представление данных позволяет проводить
Резюме
Многомерное представление данных позволяет проводить сложный анализ информации, содержащейся в базах данных. Основой многомерного представления является группирующий запрос (с оператором GROUP BY). С точки зрения пользователя анализ с помощью многомерного представления данных очень прост в использовании.
Специализированный редактор компонента TDecisionQuery
Рисунок 30.2. Специализированный редактор компонента TDecisionQuery
Из списка доступных полей при помощи кнопок Add требуемые поля можно перенести в список полей — размерностей Dimensions и список суммирующих полей Summaries. Поля из этих списков используются при создании запроса.
Запрос формируется автоматически при работе с описанными элементами управления. Текст запроса доступен для просмотра и редактирования на странице SQL Query.
Подготовка набора данных к многомерному представлению осуществляется компонентом TDecisionCube. Его основная задача — создание размерностей для многомерной структуры данных на основе каждого поля набора данных. Для каждой размерности в компоненте можно задать ряд параметров, которые определяют ее поведение и внешний вид.
Компонент TDecisionCube формирует размерности при открытии набора данных, причем созданное многомерное представление данных полностью работоспособно уже во время разработки. Для этого достаточно присвоить свойству Active Компонентов TDecisionQuery или TQuery значение True. После этого любой визуальный компонент многомерного представления начинает работать так же, как и во время выполнения.
Компонент TDecisionCube также позволяет управлять использованием памяти многомерного представления данных. Дело в том, что при добавлении к многомерному представлению новой размерности объем занимаемой памяти возрастает в арифметической прогрессии. Поэтому возможность ограничения размеров используемой памяти особенно актуальна для больших наборов данных.
Все основные настройки компонента выполняются при помощи специализированного редактора свойства DecisionMap (Рисунок 30.3).
Специализированный редактор свойства
Рисунок 30.3. Специализированный редактор свойства DecisionMap компонента TDecisionCube (страница Dimension Settings)
Для настроек размерностей используется страница Dimension Settings этого редактора. В расположенном слева списке Available Fields содержатся все поля набора данных. В элементах управления справа приведены параметры размерности для выбранного поля.
В однострочном редакторе Display Name задается название поля, которое будет присутствовать в визуальных компонентах многомерного просмотра.
Неактивный список выбора Туре показывает, является ли поле основой для размерности или суммы.
Список выбора Active Type определяет, когда данные поля появляются в визуальных компонентах. Его элементы обозначают следующее:
Active — данные поля видны сразу после открытия формы и набора данных во время выполнения или сразу после открытия набора данных во время разработки; AS Needed — данные поля становятся видны после выполнения пользователем во время выполнения или разработчиком во время разработки действий по отображению данных; inactive — данные поля не видны.Однострочный редактор Format содержит строку форматирования для данных поля.
Комбинированный список Grouping необходим для того, чтобы определить, какие значения будут показаны. Варианты Year, Quarter, Month возможны только для полей с календарным типом данных.
Однострочный редактор Initial Value задает начальное значение для поля.
Страница Memory Control используется для управления расходом памяти для нужд компонента (Рисунок 30.4). Однострочные редакторы Dimensions, Summaries и Cells в ряду Maximum позволяют задать максимальное число размерностей, сумм и ячеек, соответственно.
Специализированный редактор свойства DecisionMap компонента TDecisionCube (страница Memory Control)
Рисунок 30.4. Специализированный редактор свойства DecisionMap компонента TDecisionCube (страница Memory Control)
Аналогичные значения в ряду Current показывают текущее число этих структур.
Аналогичные значения в ряду Active+Needed показывают общее возможное число размерностей, сумм и ячеек.
Значения в ряду Active показывают число видимых размерностей сумм и ячеек.
Кнопка Get Cell Counts выполняет запрос, который возвращает число ячеек в кросстабе.
Группа радиокнопок Designer Data Options задает режим показа данных во время разработки:
Display Dimension Names — отображаются только названия размерностей; Display Names and Values — отображаются названия размерностей и значения; Display Names, Values, and Totals — отображаются названия, значения и суммы размерностей; Run Time Display Only — визуализация данных осуществляется только во время выполнения.Подготовленный к использованию для многомерного отображения набор данных необходимо связать с визуальными компонентами. Это делается при помощи компонента TDecisionSource. Через один такой компонент с набором данных можно связать несколько визуальных компонентов (см. табл. 30.1).
Кроме этого, компонент TDecisionSource позволяет разработчику установить общие для всех связанных с ним визуальных компонентов многомерного представления данных настройки отображения данных.
Как связать компоненты многомерного представления данных
Таблица 30.1. Как связать компоненты многомерного представления данных
Свойство |
Значение |
Описание |
TBecisionCube |
||
DataSet |
DecisionQuery1 |
Определяет компонент доступа к данным, который создает набор данных |
TDecisionSource |
||
DecisionCube |
DecisionCubel |
Указывает на компонент формирования многомерного набора данных |
TDecisionGrid |
||
DecisionSource |
DecisionSourcel |
Ссылается на компонент TDecisionSource |
TDecisionPivot |
DecisionSource |
DecisionSourcel |
Ссылается на компонент TDecisionSource |
Если задать текст запроса SQL и открыть набор данных, то вся цепочка заработает, причем ее поведение ничем не отличается от поведения во время выполнения приложения.
Теперь, когда мы узнали, как объединить компоненты многомерного представления данных в единую систему, настало время более подробно изучить возможности каждого компонента.
Свойства и методы компонента TDecisionCube
Таблица 30.2. Свойства и методы компонента TDecisionCube
Объявление |
Тип |
Описание |
Свойства |
||
property Active: Boolean; |
Pu |
Разрешает или запрещает преобразование набора данных в кросстаб |
property BinData: Boolean; |
Ro |
Значение True означает, что хотя бы одна размерность находится в свернутом состоянии (данные не отображаются) |
property Capacity: Integer; |
Pu
|
Определяет число байтов, используемых для хранения многомерного массива |
property CurrentSuramary: Integer; |
Pu |
Содержит индекс текущей суммы кросстаба |
property DataSet: TDataSet; |
Pb |
Ссылка на экземпляр набора данных, который отображается в кросстабе |
type TCubeDesignState = (dsNoData, dsMetaData, dsDimensionData, dsAHData); property DesignState: TCubeDesignState; |
Pu |
Задает режим отображения данных в кросстабе: dsNoData — во время разработки данные не видны; dsMetaData — видны названия размерностей; dsDimensionData — видны названия размерностей и значения, суммы не видны; dsAHData — видны все данные |
property DimensionCount: Integer; |
Ro |
Возвращает число размерностей |
property DimensionMap: TCubeDims; |
Pb |
Индексированный список ссылок на объекты параметров размерностей |
property DimensionMapCount: Integer; |
Pb |
Общее число полей набора данных, включая поля размерностей и сумм |
property MaxCells: Integer; |
Pb |
Задает максимальное число ячеек кросстаба |
property MaxDimensions: Integer; |
Pb |
Задает максимальное число размерностей |
property MaxSummaries: Integer; |
Pb |
Задает максимальное число сумм |
property ShowProgressDialog: Boolean; |
Pb |
При значении True при подготовке кросстаба отображается индикатор |
property SummaryCount: Integer; |
Ro |
Возвращает число активных сумм кросстаба |
Методы |
||
function GetDetailSQL (ValueArray : TSmalllntArray; SelectList: string; bActive: Boolean) : string; |
Pu |
Возвращает текст запроса SQL, который может быть использован для создания набора данных, включающего данные из кросстаба без сумм |
function GetSQL(ValueArray: TSrralllntArray; bActive: Boolean): string; |
Pu |
Возвращает текст запроса SQL, который может быть использован для создания набора данных, включающего данные из кросстаба без сумм |
procedure ShowCubeDialog; |
Pu |
Вызывает специализированный редактор компонента |
procedure Refresh (DimensionMap : TCubeDims; bForce: Boolean) ; |
Обновляет список объектов параметров размерностей |
|
Методы-обработчики событий |
||
type TCubeRefreshEvent = procedure (DataCube: TCustomDataStore; DimMap: TCubeDims) of object; property OnRefresh: TCubeRefreshEvent; property AfterClose: TCubeNotifyEvent; |
Pb |
Вызывается сразу после закрытия компонента (Active := False) |
property AfterOpen: TCubeNot if yEvent; |
Pb |
Вызывается сразу после открытия компонента (Active := False) |
property BeforeClose: TCubeNotif yEvent ; |
Pb |
Вызывается перед закрытием компонента (Active := False) |
property BeforeOpen: TCubeNotifyEvent; |
Pb |
Вызывается перед открытием компонента (Active := False) |
TErrorAction = (eaFail, eaContinue) ; TCapacityErrorEvent = procedure (var EAction: TErrorAction) of object; property OnLowCapacity: TCapacityErrorEvent ; |
Pb |
Вызывается после того, как занимаемый кросстабом объем памяти превысит заданный предел |
При помощи методов GetDetailsQL и GetSQL можно получить тексты запросов, которые возвращают набор данных, соответствующий кросстабу с заданным параметрами состояниями. Массив vaiueArray содержит условия для полей размерностей. Первой размерности соответствует первый элемент массива, второй размерности — второй элемент и т. д. Если значение элемента меньше нуля, то в результат запроса попадают все значения поля размерности. Значение элемента, равное или больше нуля, определяет индекс значения поля размерности. Параметр selectList содержит разделенный запятыми список дополнительных полей, которые нужно включить в запрос. Параметр bActive накладывает дополнительное ограничение на размерности. При значении True в результат запроса автоматически (без использования параметра selectList) попадают только активные размерности.
Ключевым свойством компонента является свойство DecisionMap, которое позволяет установить параметры размерностей и максимальный размер используемой памяти. Для этих целей применяется специализированный редактор (см. Рисунок 30.4).
Это свойство представляет собой экземпляр класса TCubeDims, который инкапсулирует индексированный список экземпляров объектов TCubeDim, каждый из которых содержит информацию о параметрах одной размерности. Основные свойства этого класса представлены в табл. 30.3.
Основные свойства класса TCubeDim
Таблица 30.3. Основные свойства класса TCubeDim
Объявление |
Тип |
Описание |
type TActiveFlags = (diActive, diAsNeeded, dilnactive); property ActiveFlag: TActiveFlags; |
Pb |
Определяет режим отображения данных размерности
|
property BaseName: string; |
Pb |
Содержит имя поля размерности в таблице базы данных |
property BinFormat: string; |
Pu |
Определяет способ форматирования диапазона значений размерности |
type TBinType = (binNone, binYear, binQuarter, binMonth, binSet, binCustom) ; property BinType: TBinType; |
Pb |
Определяет способ группирования данных в размерности |
type TDimFlags = (dimDimension, dimSum, dimCount, dimAverage, dimMin, dimMax, dimGenericAgg, dimUnknown) ; property DimensionType: TDimFlags ; |
Pb |
Определяет тип размерности |
property FieldName: String; |
Pb |
Содержит имя поля в наборе данных |
property FieldType: TFieldType; |
Pu |
Определяет тип поля |
property Format: String; |
Pu |
Задает форматирование данных размерности |
property Loaded: Boolean; |
Ro |
Значение True говорит о том, что данный элемент загружен в многомерный набор данных |
property StartDate: TDate; |
Pu |
Определяет начальный элемент для группировки по дате |
property StartValue: String; |
Pu |
Определяет начальный элемент : для группировки по значению |
property ValueCount: Integer; |
Pb |
Возвращает число уникальных элементов в размерности |
Основные свойства компонента TDecisionSource
Таблица 30.4. Основные свойства компонента TDecisionSource
Объявление |
Описание |
type TDecisionControlType= (xtCheck, xtRadio, xtRadioEx) ; property ControlT ype: TDecisionControlType; |
Определяет способ управления отдельной размерностью в компоненте TDecisionGrid |
property CurrentSum: Integer; |
Содержит индекс текущей суммы в компоненте TDecisionGrid |
property DecisionCube: TDecisionCube; |
Связывает данный компонент с компонентом TDecisionCube. Содержит ссылку на экземпляр компонента TDecisionCube |
property Ready: Boolean; |
Значение True означает, что данный компонент связан с активным компонентом TDecisionCube |
property SparseCols: Boolean; |
При значении True из компонента TDecisionGrid удаляются пустые колонки |
property SparseRows: Boolean; |
При значении True из компонента TDecisionGrid удаляются пустые строки |
Компонент имеет средства для управления состоянием многомерного представления в визуальных компонентах. Для этого используется свойство ControlType. Рассмотрим его возможные значения:
xtcheck — щелчок на кнопках размерности в компонентах TDecisionGrid и TDecisionPivot приводит к открытию или закрытию размерности; xtRadio — щелчок на кнопках размерности в компонентах TDecisionGrid и TDecisionPivot приводит к открытию или закрытию данной размерности и закрытию всех остальных в этом направлении; xtRadioEx — щелчок на кнопках размерности в компонентах TDecisionGrid и TDecisionPivot приводит к открытию или закрытию данной размерности и закрытию или открытию всех остальных размерностей в этом направлении.Свойства и методы компонента TDecisionGrid
Таблица 30.5. Свойства и методы компонента TDecisionGrid
Объявление |
Тип |
Описание |
Свойства | ||
property Cells [ACol, ARow: Integer] : string; |
Ro |
Индексированный массив значений всех ячеек компонента в строковом формате |
property ColCount : Integer; |
RO |
Возвращает общее число колонок в сетке |
property DecisionSource: TDecisionSource; |
Рb |
Указывает на компонент TDecisionSource, через который осуществляется связь с набором данных |
property Dimensions: TDisplayDims; |
Pb |
Объект TDisplayDims представляет индексированный список объектов визуальных свойств размерностей |
property FixedCols: Integer; |
Ro |
Возвращает число фиксированных колонок, которые используются для отображения информации о размерностях (названия, значения, обозначения) |
property FixedRows: Integer; |
Ro |
Возвращает число фиксированных строк, которые используются для отображения информации о размерностях (названия, значения, обозначения) |
type TDecisionGridOption = (cgGridLines, cgOutliner, cgPivo table) ; TDecisionGridOptions = set of TDecisionGridOption; property Options: TDecisionGridOptions; |
Pb |
Определяет общие настройки компонента: cgGridLines — отображаются вертикальные и горизонтальные разделительные линии; cgOutliner — отображаются элементы управления в виде знаков "+" и "-" для открытия и закрытия размерностей; cgpivotable — размерности можно переупорядочивать при помощи перетаскивания |
property RcwCount: Integer; |
Ro |
Возвращает общее число строк в сетке |
property ShowCubeEditor: Boolean; |
Pb |
Разрешает или запрещает использование специализированного редактора компонента TDecisionCube |
property Totals: Boolean; |
Pu |
При значении True сетка имеет промежуточные суммы по каждой колонке и строке |
Методы |
||
type TDecisionDrawStates = i (dsGroupStart, dsRowCaption, dsColCaption, dsSum, dsRowValue, dsColValue, dsData, dsOpenAfter, dsCloseAfter, dsCloseBefore, dsOpenBefore, dsRowIndicator, dsColIndicator, dsRowPlus, dsColPlus, dsNone); TDecisionDrawState = set of TDecisionDrawStates; function CellDrawState(ACol, ARow: Integer; var Value: string; var DrawState: TDecisionDrawState) : boolean; |
Pu |
Позволяет определить назначение любой ячейки сетки. Параметры ACol и ARow определяют положение ячейки в сетке. В параметре Value возвращается строка, содержащая значение в том виде, как оно представлено в ячейке. Параметр DrawState возвращает информацию о назначении ячейки |
function CellValueArray (ACol, ARow: Integer; var ValueArray: TValueArray): boolean; |
Pu |
Возвращает индексы всех полей, данные которых суммированы в ячейке. Параметр ValueArray содержит индексы полей |
Компонент TDecisionGrid является предком класса TCustomGrid и поэтому обладает всеми базовыми свойствами и методами, присущими сетке.
Для доступа к значению каждой ячейки используется свойство cells. Адресация ячеек осуществляется с левой верхней ячейки, которая имеет индексы [0,0].
Свойство Dimensions является экземпляром объекта TDisplayDims, который инкапсулирует индексированный список указателей на экземпляры объектов TDispiayDim. Каждый такой объект содержит важнейшие визуальные свойства размерностей. При щелчке на кнопке в однострочном редакторе свойства в Инспекторе объектов разворачивается список всех таких объектов.
Свойства и методы компонента TDecisionPivot
Таблица 30.6. Свойства и методы компонента TDecisionPivot
Объявление |
Тип |
Описание |
Свойства |
||
property DecisionSource: TDecisionSource; |
Рb |
Определяет компонент TDecisionSource, через который осуществляется управление многомерным представлением данных |
type TDecisionButtonPosition = (xtHorizontal, xtVertical, XtLeftTop) ; property GroupLayout: TDecisionButtonPosition; |
Рb |
Задает способ расположения кнопок на панели: xtHorizontal — в ряд слева направо; xtVertical — в колонку сверху вниз; xtLeftTop — кнопки вертикальных размерностей вдоль верхнего края, кнопки горизонтальных размерностей вдоль левого края, кнопка сумм в левом верхнем углу |
type TDecisionPivotOption = (xtRows, xtColumns, xtSuiranaries); TDecisionPivotOptions = set of TDecisionPivotOption; property Groups : TDecisionPivotOptions; |
РЬ |
Управляет видимостью трех групп кнопок |
property GroupSpacing: Integer; |
Pb |
Определяет размер в пикселах промежутка между группами кнопок |
Методы |
||
procedure SetBounds (Left, Top, Height, Width: Integer); override; |
Pu |
Переустанавливает размеры компонента в соответствии с параметрами метода |
Методы-обработчики компонента унаследованы от класса TWinControl.
Управление данными
Управление данными
Несомненное преимущество многомерного представления данных в том, что пользователь может легко изменить взаимное положение размерностей одной стороны между собой и переносить размерности с горизонтали на вертикаль и обратно. Для того чтобы сделать размерность видимой или невидимой, пользователю достаточно щелкнуть на кнопке.
Взаимное положение и расположение размерностей по сторонам кросстаба никак не связано с местом полей в запросе компонента TDecisionQuery.
Все операции по управлению многомерным представлением сосредоточены в одном компоненте — TDecisionPivot (см. Рисунок 30.2). В некоторой степени это аналог компонента TDBNavigator, только TDecisionPivot управляет не записями набора данных, а размерностями многомерного представления данных.
Этот компонент подключается к общей цепочке компонентов многомерного представления данных через компонент TDecisionSource. Поэтому любые действия с компонентом TDecisionPivot немедленно отражаются во всех визуальных компонентах, которые также подключены к этому экземпляру TDecisionSource.
Взаимосвязь компонентов многомерного представления данных
Взаимосвязь компонентов многомерного представления данных
При создании в приложении формы для многомерного представления данных следует помнить, что при этом обязательно должны решаться следующие задачи:
должен быть создан группирующий и суммирующий запрос SQL, обеспечивающий открытие набора данных для кросстаба; перед отображением данных необходимо настроить параметры размерностей кросстаба; непосредственный показ данных в кросстабе; работающий кросстаб должен эффективно управляться на уровне размерностей.Для этого в форме приложения требуется разместить как минимум пять компонентов со страницы Decision Cube Палитры компонентов.
Для создания запроса SQL можно использовать компонент TDecisionQuery или обычный компонент TQuery.
Запрос должен быть связан с компонентом TDecisionCube, который осуществляет подготовку набора данных запроса к многомерному показу.
Для соединения многомерного набора данных с компонентом отображения данных используется компонент TDecisionSource — полный функциональный аналог TDataSource. Этот компонент, в свою очередь, должен связываться и с набором данных, и с инструментом многомерного представления данных.
Непосредственный показ многомерного набора данных проводится при помощи компонентов TDecisionGrid и TDecisionGraph. Они должны поддерживать соединение с компонентом TDecisionSource.
Наконец, управление многомерным представлением данных реализует компонент TDecisionPivot, он также должен быть связан с компонентом TDecisionSource.
Допустим, что на форме расположены следующие компоненты:
TDecisionQuery по имени DecisionQuery1; TDecisionCube по имени DecisionCubel; TDecisionSource по имени DecisionSourcel; TDecisionGrid по имени DecisionGrid1; TDecisionPivot по имени DecisionPivotl.Тогда для того, чтобы связать все эти компоненты в единый работающий механизм многомерного представления данных, нужно установить значения для их важнейших свойств. Значения свойств представлены в табл. 30.1.
Диалоговое окно выбора папки, созданное при вызове функции ShBrowseForFolder
Рисунок 31.3. Диалоговое окно выбора папки, созданное при вызове функции ShBrowseForFolder
В данном примере корневой служит виртуальная папка My Computer. Пользователю предоставляется возможность выбрать одну из папок файловой системы (за это отвечает флаг TBrowseinfo.uiFlags, равный
BIF_RETURNONLYFSDIRS).
На выходе функция возвращает pidi папки, имя которой извлекается из него вызовом еще одной функции Shell — shGetPathFromList.
procedure TForml/ButtonlClick(Sender: TObject) ;
var
BI : TBrowselnfo;
Image : integer;
StartPIDL, ResPIDL : PItemlDList;
S, Path : ArraytO..max_path-l] Of WideChar;
begin
01eCheck(SHGetSpecialFolderLocation(Handle, CSIDL_DRIVES, StartPIDL));
With BI do
Begin
hwndOwner = Application.Handle;
pszDisplayName = @S;
IpszTitle = 'Выберите необходимую папку';
ulFlags = BIF_RETURNONLYFSDIRS;
pidlRoot = StartPIDL;
Ipfn = nil;
iImage = 1;
end;
ResPIDL := SHBrowseForFolder(BI) ;
if SHGETPathFromlDList(ResPIDL, @Path[0])
then Labe11.Caption := StrPas(@Path[0]) ;
end;
Полученное имя здесь отображается при помощи компонента Label 1.
3. Наконец, перейдем к третьему действию нашей задачи. Теперь, зная pidi папки, с которой вы будете работать, можно получить указатель на интерфейс ishellFolder вызовом метода BindToObject. Мы еще не рассмотрели такой важный аспект работы с папками, как просмотр их содержимого. Верные правилу СОМ: "каждый должен заниматься своим делом", разработчики Shell предоставили для просмотра еще один интерфейс — IEnumiDList. Пугаться нечего, набор возможностей этого интерфейса даже меньше, чем у пульта ДУ в магнитофоне. Его четыре метода — Next, Skip, Reset и clone — позволяют организовать просмотр списка в одном направлении, а также возврат к началу и дублирование (Clone) выбранного элемента списка. Вот как это выглядит на практике.
Memol.Clear; try
01eCheck(SHGetDesktopFolder(DeskTop));
if not Succeeded(DeskTop.ParseDisplayName
(Self.Handle,nil, StringToWideChar (Editl.Text,ws, MAX_PATH),n, pidi, attr))
then begin ShowMessage('Неизвестное имя');
Exit; end; OleCheck(DeskTop.BindToObject(pidl,nil, IID_IShellFolder, Pointer(NewShellFolder)});
OleCheck(NewShellFolder.EnumObj ects{Self.Handle,
SHCONTF_FOLDERS or SHCONTF_NONFOLDERS, Enumerator)); while Enumerator.Next(1, pidl, Numpidls) = S_OK do
begin
NewShellFolder.GetDisplayNameOf(PIDL, SHGDN_FORPARSING, StrRet); case StrRet.uType of STRRET_CSTR:
s := StrRet.cStr; STRRET_OFFSET:
begin
P := @PIDL.mkid.abID[StrRet.uOffset - SizeOf(PIDL.mkid.cb)];
SetString(s, P, PIDL.mkid.cb - StrRet.uOffset);
end; STRRET_WSTR:
s := StrRet.pOleStr;
end;//case
Memol.Lines.Add(s);
end; except
on ErEOleSysError do ShowMessage('');
end;
В этом примере имя нужной папки извлекается из компонента Edit1. Получив указатель на интерфейс ishellFoider и затем интерфейс IEnumiDList, программа заполняет полученными именами файлов список Memol.Lines.
Помимо названия из большинства объектов файловой системы можно "вытащить" массу полезной информации. Чаще всего задаются вопросом: а как извлечь значок, соответствующий данному файлу или хранящийся в нем?
Способов для достижения этой цели несколько. Самый простой — через вызов функции:
function SHGetFileInfo(pszPath: PAnsiChar; dwFileAttributes: DWORD;
var psfi: TSHFilelnfo; cbFilelnfo, uFlags: UINT): DWORD;
Параметр pszPath может быть указателем как на строку с именем файла, так и на структуру вида pidl. Функция заполняет структуру psfi (тип TSHFilelnfo) длиной cbFilelnfo байт. В зависимости от значения слова флагов (параметр uFlags) на выходе может быть разнообразная информация. В частности, если в параметре uFlags заданы значения SHGFI_SYSICONINDEX и SHGFI_ICON, то в структуру psfi будет записан номер значка для данного файла в системном списке изображений, а результатом выполнения функции будет дескриптор этого списка. Воспользоваться им можно (например, для панели инструментов) так:
procedure TForml.FormCreate(Sender: TObject);
var
Filelnfo: TSHFilelnfo;
ImageListHandle: THandle;
begin
ImageListHandle := SHGetFilelnfo('С:\',
0,
Filelnfo, SizeOf(Filelnfo) ,
SHGFI_SYSICONINDEX or SHGFI_ICON);
SendMessage(ToolBarl.Handle, TB_SETIMAGELIST, 0, ImageListHandle);
end;
Точно так же можно извлечь значок, соответствующий конкретному файлу. В составе Shell есть другие функции, созданные для извлечения значков:
function Extractlcon(hlnst: HINST; IpszExeFileName: PChar; nlconlndex: UINT): HICON;Эта функция извлекает значок из файла IpszExeFileName (это должен быть файл типа EXE, DLL или ICO) и возвращает его дескриптор. Если значок не найден, возвращаемое значение равно 0.
function ExtractAssociatedIcon(hInst: HINST; IpIconPath: PChar; var Ipilcon: Word): HICON;Эта функция может работать с файлами разных форматов. Сначала она, как и предыдущая, ищет значок в теле файла. Если его там нет, предпринимается попытка отыскать значок в приложении, связанном с данным типом файлов. Например, из файла с расширением doc будет извлечен один из значков Microsoft Word.
Добавление пунктов в системное контекстное меню
Добавление пунктов в системное контекстное меню
Вы обращали внимание на то, что некоторые приложения после установки добавляют в системное контекстное меню свои собственные пункты? Так поступают многие архиваторы, антивирусные средства и другие утилиты. Эта возможность предоставляется оболочкой Windows.
Когда пользователь щелкает правой кнопкой мыши на любом объекте в пространстве имен, система создает контекстное меню из двух частей: стандартного меню для объектов данного типа и пунктов меню, добавляемых зарегистрированными обработчиками. Зарегистрированные обработчики — это СОМ-серверы, запускаемые в адресном пространстве процесса (in-process servers) и реализованные в виде динамических библиотек.
Ваш СОМ-объект, который расширяет системное контекстное меню, должен поддерживать как минимум два интерфейса — ishellExtinit и IContextMenu. существует и два новых интерфейса — IContextMenu2 и icontextMenuS, но они вносят в логику работы контекстных меню лишь небольшие дополнения и здесь рассмотрены не будут. Интерфейс ishellExtinit отвечает за инициализацию меню, а интерфейс IContextMenu — за выполнение основных функций.
Методы интерфейса IContextMenu приведены в табл. 31.3.
Компоненты Rave Reports и отчеты в приложении Delphi
ГЛАВА 31
Использование возможностей Shell API
Разобравшись с механизмами СОМ, вам наверняка захочется "испытать радость общения" с объектами, имеющимися в составе ОС. Microsoft уверенно идет к тому, чтобы все составные части своих операционных систем, как и прочих продуктов, превратить в СОМ-объекты. В этом направлении сделаны большие шаги, и оболочка Windows, и ее файловая система предоставляют интерфейсы СОМ. В Windows 2000, судя по заверениям представителей фирмы, все новые возможности представлены и доступны в виде интерфейсов.
В качестве примера работы с интерфейсом ShellLink вместе с Delphi поставляется приложение Virtual ListView. Но, во-первых, в нем безо всякого документирования вводятся достаточно сложные структуры и интерфейсы; во-вторых, оно содержит только минимум функций для работы с объектами. В этой главе мы постараемся объяснить применяемые там приемы.
Примечание
Примечание
Интерфейсы функций и СОМ-объектов Shell содержатся в модулях SHELLAPI.PAS и SHLOBJ.PAS, которые имеются в поставке Delphi.
Интерфейс IShellFolder
Интерфейс IShellFolder
Этот интерфейс соответствует папке — одному из основных элементов пространства имен Проводника. Зачем было вводить термин "папка", когда существовали уже общепринятые "каталог" и "директория"? В отличие от последних двух, папка может быть не просто обычным элементом файловой системы. Она может быть виртуальной — как папки Принтеры, Документы или Панель управления. Любая папка может содержать коллекцию объектов из состава пространства имен.
Получив указатель на интерфейс ishellFoider, соответствующий папке, вы можете работать с ней, как с объектом СОМ. "Верхушкой" (корневой папкой) пространства имен является папка Рабочий стол (Desktop). Получить интерфейс isheiiFoider этой папки можно путем вызова функции:
function SHGetDesktopFolder(var ppshf: IShellFolder): HResult;
Логика работы с описываемым интерфейсом такова: сначала необходимо получить интерфейс нужной папки, а затем можно переходить к работе с ее содержимым. Содержимое представляет собой список, а каждый элемент папки представлен структурой pitemiDList. Эта структура не типизирована; ее единственное обязательное поле содержит длину в байтах, зная которую можно переместиться к следующему элементу. То есть получается обычная цепочка. Все остальные поля заполняются соответствующими функциями и методами интерфейса ishellFoider.
Примечание
Примечание
Все служебные функции работы со структурами PitemiDList — создание, уничтожение, копирование, перемещение по цепочке и т. п. — содержатся в примере Virtual ListView, поставляемом с Delphi. Если вы намерены писать программы, работающие с ishellFoider, целесообразно взять их на заметку. В дальнейшем для простоты эти структуры будем именовать pidl.
Рассмотрим функции интерфейса ishellFoider. Под "текущей папкой" в табл. 31.1 понимается та папка, которая в данный момент представляет интерфейс IShellFolder.
Интерфейс IShellLink
Интерфейс IShellLink
Этот интерфейс представляет собой средство для создания и управления ярлыками (shortcuts). Все читатели этой главы наверняка создавали и перемещали ярлыки для наиболее нужных программ, файлов и папок — на рабочем столе, в главном меню и т. д. С точки зрения ОС эти действия — не что иное, как создание и изменение свойств СОМ-объекта.
Каждый ярлык содержит следующую информацию:
путь к объекту, на который ссылается ярлык (Path); рабочий каталог для этого объекта (Working Directory); список параметров, передаваемый объекту при его активизации (Arguments); начальное состояние окна, соответствующего объекту (нормальное, минимизированное, максимизированное) (ShowCmd); путь к значку, соответствующему объекту (Icon Location); описание объекта (Description); сочетание "горячих" клавиш (HotKey).Для всех этих свойств ярлыка в интерфейсе дано по паре методов — один для чтения, другой для установки значения:
IShellLink = interface(lUnknown)
{ si }
[SID_IShellLinkA]
function GetPath(pszFile: PAnsiChar;
cchMaxPath: Integer; var pfd: TWin32FindData;
fFlags: DWORD): HResult; stdcall;
function GetlDList(var ppidl: PItemlDList): HResult; stdcall;
function SetlDList(pidl: PItemlDList): HResult;
stdcall;
function GetDescription(pszName: PAnsiChar;
cchMaxName: Integer): HResult;
stdcall;
function SetDescription(pszName: PAnsiChar): HResult; stdcall;
function GetWorkingDirectory(pszDir: PAnsiChar; cchMaxPath: Integer): HResult; stdcall;
function SetWorkingDirectory(pszDir: PAnsiChar): HResult; stdcall;
function GetArguments(pszArgs: PAnsiChar; cchMaxPath: Integer): HResult; stdcall;
function SetArguments(pszArgs: PAnsiChar): HResult; stdcall;
function GetHotkey(var pwHotkey: Word): HResult; stdcall;
function SetHotkey(wHotkey: Word): HResult; stdcall;
function GetShowCmd(out piShowCmd: Integer): HResult; stdcall;
function SetShowCmd(iShowCmd: Integer): HResult; stdcall;
function GetIconLocation(pszIconPath: PAnsiChar; cchlconPath: Integer; out pilcon: Integer): HResult; stdcall;
function SetlconLocation(pszIconPath: PAnsiChar; ilcon: Integer): HResult; stdcall;
function SetRelativePath(pszPathRel: PAnsiChar; dwReserved: DWORD): HResult; stdcall;
function Resolve(Wnd: HWND; fFlags: DWORD): HResult; stdcall;
function SetPath(pszFile: PAnsiChar): HResult; stdcall;
end;
Сохраним ярлык для данной программы-примера где-нибудь на диске, скажем, в той же самой папке. Для этого создадим новый объект NewLink класса CLSiD_shellLink, предоставляющий нам нужный интерфейс:
procedure TForml.ButtonlClick(Sender: TObject);
var NewLink : IShellLink;
fn, fp : string; ws : WideString; hRes : THandle;
pf : IPersistFile;
begin
NewLink := CreateComObject(CLSID_ShellLink) as IShellLink; fn := ParamStr(O); NewLink.SetPath(pchar(fn));
fp := ExtractFilePath(fn); NewLink.SetWorkingDirectory(pchar(fp)); NewLink.SetDescription
(pChar(Application.Title));
ws := fp+Application.Title+'.Ink';
hRes := NewLink.Querylnterface(IID__IPersistFile, pf) ; if Succeeded(hRes) then
pf.Save(pWideChar(ws),False);
end;
В этом примере помимо IShellLink нужно получить доступ к интерфейсу IPersistFile, который "умеет" записывать данные. Задав параметры ярлыка, мы записываем его на диск. При этом проверяется тот факт, что созданный нами объект поддерживает интерфейс IPersistFile. Если указатель на этот интерфейс получен, вызывается его метод save.
Среди перечисленных выше методов IShellLink особое внимание уделим методу Resolve. Он понадобится вам при получении указателя на интерфейс уже существующих ярлыков. Windows пытается вести себя "разумно" и отслеживает перемещения и переименования объекта, на который указывает существующий IShellLink. Но если вы записали содержимое ярлыка в поток (или на диск), то отследить соответствие ярлыка объекту должны сами, вызвав метод Resolve. Если объект, на который ссылается ярлык, по-прежнему находится на своем месте, метод немедленно завершается с нормальным кодом возврата. Если файл или объект перемещен или переименован, начинается его поиск (Рисунок 31.2).
Над значком, помещенным на панель System Tray, видна строка подсказки
Рисунок 31.1. Над значком, помещенным на панель System Tray, видна строка подсказки
Сообщение, задаваемое в поле uCallbackMessage, по сути дела является единственной ниточкой, связывающей вас со значком после его создания. Оно объединяет в себе несколько сообщений. Когда к вам пришло такое сообщение (в примере, рассмотренном выше, оно имеет идентификатор WM_MYTRAYNOTIFY), поля в переданной в обработчик структуре типа TMessage распределены так. Параметр wParam содержит номер значка (тот самый, что задавался в поле uID при его создании), а параметр LParam — идентификатор сообщения от мыши, вроде WM_MOUSEMOVE, WM_LBUTTONDOWN и т. п. К сожалению, остальная информация из этих сообщений теряется. Координаты мыши в момент события придется узнать, вызвав функцию API GetCursorPos:
procedure TForml.WMICON(var msg: TMessage);
var P : TPoint; begin case msg.LParam of
WM_LBUTTONDOWN:
begin
GetCursorPos(p);
SetForegroundWindow(Application.MainForm.Handle); PopupMenul.Popup(P.X, P.Y);
end;
WM_LBUTTONUP :
end;
end;
Обратите внимание, что при показе всплывающего меню недостаточно просто вызвать метод Popup. При этом нужно вынести главную форму приложения на передний план, в противном случае она не получит сообщений от меню.
Теперь решим еще две задачи. Во-первых, как сделать, чтобы приложение минимизировалось не на Панель задач (TaskBar), а на System Tray? И более того — как сразу запустить его в минимизированном виде, а показывать главную форму только по наступлении определенного события (приходу почты, наступлению определенного времени и т. п.).
Ответ на первый вопрос очевиден. Если минимизировать не только окно главной формы приложения (Application.MainForm.Handle), но и окно приложения (Application.Handle), то приложение полностью исчезнет "с экранов радаров". В этот самый момент нужно создать значок на панели System Tray. В его всплывающем меню должен быть пункт, при выборе которого оба окна восстанавливаются, а значок удаляется.
Чтобы приложение запустилось сразу в минимизированном виде и без главной формы, следует к вышесказанному добавить установку свойства Application.showMainForm в значение False. Здесь возникает одна сложность — если главная форма создавалась в невидимом состоянии, ее компоненты будут также созданы невидимыми. Поэтому при первом ее показе установим их свойство visible в значение True. Чтобы не повторять это дважды, установим флаг — глобальную переменную shownonce:
procedure TForml.HideMainForm;
begin
Appiication.showMainForm := False;
ShowWindow(Application.Handle, SW_HIDE);
ShowWindow(Application.MainForm.Handle, SW_HIDE);
end;
procedure TForml.RestoreMainForm;
var i,j : Integer;
begin
Appiication.showMainForm := True;
ShowWindow(Application.Handle, SW_RESTORE); ShowWindow(Application.MainForm.Handle, SW_RESTORE);
if not ShownOnce then begin
for I := 0 to Application.MainForm.ComponentCount -1 do if Application.MainForm.Components[I] is TWinControl then with Application.MainForm.Components[I] as TWinControl do if Visible then
begin
ShowWindow(Handle, SW_SHOWDEFAULT);
for J := 0 to ComponentCount -1 do if Components[J] is TWinControl then
ShowWindow((Components[J] as TWinControl).Handle, SW_SHOWDEFAULT);
end;
ShownOnce := True;
end;
end;
procedure TForml.WMSYSCOMMAND(var msg: TMessage);
begin inherited;
if (Msg.wParam=SC_MINIMIZE) then
begin
HideMainForm; CreateTraylcon(l) ;
end;
end;
procedure TForml.FileOpenltemlClick(Sender: TObject); begin
RestoreMainForm;
DeleteTraylcon(l);
end;
Теперь у вас в руках полноценный набор средств для работы с панелью System Tray. В заключение необходимо добавить, что все описанное реализуется не в операционной системе, а в оболочке ОС — Проводнике (Explorer). В принципе, и Windows NT 4/2000, и Windows 95/98 допускают замену оболочки ОС на другие, например DashBoard или LightStep. Там функции панели System Tray могут быть не реализованы или реализованы через другие API. Впрочем, случаи замены оболочки достаточно редки.
Поиск объекта, на который указывает ярлык
Рисунок 31.2. Поиск объекта, на который указывает ярлык
Знакомая картина, не правда ли? Особенно часто она наблюдается в том случае, если пользователь не выработал у себя привычки правильно деинсталлировать раздобытый где-то "софт", стирая его "по старинке". Между тем, за привычным диалоговым окном на рисунке стоит вызов метода ishellLink. Resolve. Если в пределах досягаемости поиска окажется файл с тем же именем и размерами, ярлык будет автоматически переадресован на него; в противном случае пользователю будет предложено использовать ближайший по характеристикам файл из просмотренных. Если вы вообще не хотите, чтобы пользователь вмешивался в процесс отыскания соответствия, при вызове метода Resolve в параметре fFlags укажите значение SLR_NO_UI — диалоговое окно появляться в этом случае не будет.
Если вы внимательно изучили рабочий стол своего компьютера, то должны были заметить там ярлыки, ссылающиеся не на файлы, а на специальные объекты — "Мой компьютер", "Сетевое окружение", "Принтеры" и т. п. Чтобы создать такой ярлык самому, нужно обращение к методу setiDList. В качестве параметра ему передается структура pitemiDList (pidi). О том, где ее взять и как заполнить, рассказано в следующем разделе.
Понятие пространства имен
Понятие пространства имен
Необходимость как-то упорядочить все те сущности, с которыми имеет дело современная ОС, всегда вставала перед разработчиками. Довольно успешный подход к этому реализован в платформе Windows. Вооружившись идеями объектного подхода, в Microsoft разбили интерфейс ОС на две части: средства поддержки пространства имен и средства его просмотра.
Под пространством имен оболочки (Shell Namespace) мы будем понимать иерархически упорядоченную совокупность имен всех объектов, которые могут быть просмотрены через средства просмотра — файлы, устройства памяти, принтеры, сетевые ресурсы. В этой совокупности могут встречаться как реально существующие объекты (папки файловой системы), так и виртуальные объекты (папки Принтеры, Мой компьютер и т. п.)- Типовым средством просмотра пространства имен является Explorer (Проводник), но можно заменить его на другое средство, в том числе собственноручно разработанное. Обе составные части являются совокупностями СОМ-объектов, они обладают полиморфизмом и легко расширяемы. Об использовании этих объектов и функций API оболочки ОС и пойдет речь в данной главе.
Размещение значка приложения на System Tray
Размещение значка приложения на System Tray
Часто программисту приходится сталкиваться с задачей написания приложения, работающего в фоновом режиме и не нуждающегося в месте на Панели задач. Если вы посмотрите на правый нижний угол рабочего стола Windows, то наверняка найдете там приложения, для которых эта проблема решена: часы, переключатель раскладок клавиатуры, регулятор громкости и т. п. Ясно, что, как бы вы не увеличивали и не уменьшали формы своего приложения, попасть туда обычным путем не удастся. Способ для этого предоставляет Shell API.
Те картинки, которые находятся на System Tray — это действительно просто картинки, а не свернутые окна. Они управляются и располагаются панелью System Tray. Она же берет на себя еще две функции: показ подсказки для каждого из значков и оповещение приложения, создавшего значок, обо всех перемещениях мыши над ним.
Весь API System Tray состоит из 1 (одной) функции:
function Shell_NotifyIcon(dwMessage: DWORD;
IpData: PNotifylconData): BOOL; PNotifylconData = TNotifylconData; TNotifylconData = record
cbSize: DWORD;
Wnd: HWND;
uID: UINT;
uFlags: UINT;
uCallbackMessage: UINT;
hlcon: HICON;
szTip: array [0..63] of AnsiChar;
end;
Параметр dwMessage определяет одну из операций: NIM_ADD означает добавление значка в область, NIM_DELETE — удаление, NIM_MODIFY — изменение.
Ход операции зависит от того, какие поля структуры TNotifyiconData будут заполнены.
Обязательным для заполнения является поле cbsize — там содержится размер структуры. Поле wnd должно содержать дескриптор окна, которое будет оповещаться о событиях, связанных со значком. Идентификатор сообщения Windows, которое вы хотите получать от системы о перемещениях мыши над значком, запишите в поле uCallbackMessage. Если вы хотите, чтобы при этих перемещениях над вашим значком показывалась подсказка, то задайте ее текст в поле szTip. В поле UID задается номер значка — каждое приложение может поместить на System Tray сколько угодно значков. Дальнейшие операции вы будете производить, задавая этот номер. Дескриптор помещаемого значка должен быть задан в поле hIcon. Здесь вы можете задать значок, связанный с вашим приложением, или загрузить свой — из ресурсов.
Примечание
Примечание
Изменить главный значок приложения можно в диалоговом окне Project/ Options на странице Application. Он будет доступен через свойство Application.Icon. Тут же можно отредактировать и строку для подсказки — свойство Application.Title.
Наконец, в поле uFlags вы должны сообщить системе, что именно вы от нее хотите, или, другими словами, какие из полей hicon, uCaiibackMessage и szTip вы на самом деле заполнили. В этом поле предусмотрена комбинация трех флагов: NIF_ICON, NIF_MESSAGE и NIF_TIP. Вы можете заполнить, скажем, поле szTip, но если вы при этом не установили флаг NIF_TIP, созданный вами значок не будет иметь строки с подсказкой.
Два приведенных ниже метода иллюстрируют сказанное. Первый из них создает значок на System Tray, а второй — уничтожает его.
const WM_MYTRAYNOTIFY = WMJJSER + 123;
procedure TForml.CreateTraylcon(n:Integer);
var nidata : TNotifyiconData;
begin
with nidata do
begin
cbSize := SizeOf{TNotifyiconData) ;
Wnd := Self.Handle;
uID := n;
uFiags := NIF_ICON or NIF_MESSAGE or NIFJTIP;
uCallBackMessage := WM_MYTRAYNOTIFY;
hicon := Application.Icon.Handle;
szTip := 'THis is Traylcon Example';
end;
Shell_NotifyIcon(NIM_ADD, @nidata);
end;
procedure TForml.DeleteTraylcon(n:Integer);
var nidata : TNotifylconData; begin
with nidata do
begin
cbSize := SizeOf(TNotifylconData);
Wnd := Self.Handle; uID := n; end;
Shell_NotifyIcon(NIM_DELETE, @nidata);
end;
Примечание
Примечание
He забывайте уничтожать созданные вами значки на System Tray. Это не делается автоматически даже при закрытии приложения. Значок будет удален только после перезагрузки системы.
Внешний вид значка, помещенного нами на System Tray, ничем не отличается от значков других приложений (Рисунок 31.1).
в этой главе, могут дать
Резюме
Несколько тем, затронутых в этой главе, могут дать лишь начальные представления о принципах работы с оболочкой Windows. Вы можете изучить составляющие ее объекты практически сколь угодно глубоко — были бы потребность да желание. Разумеется, чтобы не "наломать дров", перед этим надо отдать себе отчет в полном и правильном понимании механизмов СОМ.
Функции интерфейса IShellFolder
Таблица 31.1. Функции интерфейса IShellFolder
Метод |
Описание |
function ParseDisplayName (hwndOwner : HWND; pbcReserved: Pointer; IpszDisplayName: POLESTR; out pchEaten: ULONG; out ppidl: PitemiDList; var dwAttributes : ULONG) : HResult; |
Эта функция позволяет получить указатель на элемент ppidl, зная только его полное имя (с путем) IpszDisplayName |
function EnumObjects (hwndOwner: HWND; grf Flags: DWORD; out EnumlDList: lEnumlDList) : HResult; |
Возвращает указатель на специальный интерфейс lEnumlDList, предназначенный для организации цикла по всем элементам списка в текущей папке |
function BindToObject (pidl: PitemiDList; pbcReserved: Pointer; const riid: TIID; out ppvOut: Pointer) : HResult; |
Возвращает интерфейс папки pidl, которая должна находиться в текущей папке (на которую ссылается интерфейс, вызвавший этот метод) |
function ComparelDs (IParam: LPARAM; pidll, pid!2: PitemiDList): HResult; |
Сравнивает два первых элемента В списках pidll И pidl2 |
function CreateViewObject (hwndOwner: HWND; const riid: TIID; out ppvOut: Pointer) : HResult; |
Создает визуальный объект для текущей папки и возвращает указатель на него в параметре ppvOut |
function GetAttributesOf (cidl: UINT; var apidl: PItemlDList; var rgflnOut: UINT): HResult; |
Возвращает атрибуты элемента под номером cidl в списке apidl. Результат — набор флагов, устанавливаемых в параметре rgf inOut |
function GetUIObjectOf (hwndOwner : HWND; cidl: UINT; var apidl: PItemlDList; const riid: TIID; prgflnOut: Pointer; out ppvOut: Pointer) : HResult; |
Создает объект пользовательского интерфейса, связанный с элементом списка aplidl под номером cidl |
function GetDisplayNameOf (pidl: PItemlDList; uFlags: DWORD; var IpName: TStrRet) : HResult; |
Возвращает имя элемента pidl. Полнота возвращаемой информации определяется параметром uFlags |
function SetNaraeOf (hwndOwner: HWND; pidl: PItemlDList; IpszName: POLEStr; uFlags: DWORD; var ppidlOut: PItemlDList) : HResult; |
Задает новое имя IpszName для списка pidl. При этом возвращается новый указатель на список — ppidlOut |
Два метода — ParseDisplayName и GetDisplayNameOf — взаимно дополняют друг друга. Первый из них нужен, если вы имеете указатель на ishellFoider и хотите связать его с конкретной папкой. На практике это сводится к задаче в три действия:
1. Получить указатель на интерфейс какой-либо папки, скажем, рабочего стола при помощи ShGetDesktopFolder.
2. Получить указатель (pidl) нужного вам элемента. Это осуществимо многими способами. Первый из них — как раз через вызов метода IShellFolder. ParseDisplayName. Если вы хотите получить доступ к одной из виртуальных (специальных) папок, то незаменимой будет следующая функция:
function SHGetSpecialFolderLocation(hwndOwner: HWND; nFolder: Integer; var ppidl: PItemlDList): HResult;
В параметре nFolder вы задаете константу, соответствующую выбранной специальной папке. На выходе будет указатель на элемент ppidl, соответствующий этой папке.
Примечание
Примечание
Во многих функциях Shell API и методах его интерфейсов встречается параметр hwndOwner. Он должен задавать дескриптор окна на тот случай, если придется выводить диалоговое окно или окно с сообщением об ошибке.
Возможные значения параметра nFolder перечислены в табл. 31.2. В комментариях к ним "виртуальная" папка является особым объектом, который предоставляется пользователю при помощи Shell API. Просто "папка" реально существует где-то в файловой системе.
Константы, определяющие специальные папки
Таблица 31.2. Константы, определяющие специальные папки
Значение |
Комментарий |
CSIDL_BITBUCKET |
Корзина (Recycle bin) — специальная папка для удаленных файлов. Пути к Recycle bin нет в системном реестре во избежание перемещения или удаления, и его не узнать иным методом |
CSIDL_CONTROLS |
Панель инструментов (Control Panel) — виртуальная папка, содержащая значки апплетов Панели инструментов |
CSIDL_DESKTOP |
Виртуальная папка Рабочий стол (Desktop), корневая в пространстве имен |
CSIDL_DESKTOPDIRECTORY |
Папка файловой системы, реально содержащая объекты рабочего стола |
CSIDL DRIVES |
Виртуальная папка Мой компьютер (My Computer), содержащая элементы для всех накопителей на компьютере подключенных сетевых устройств, папки Принтеры, Панель инструментов, Удаленный доступ к сети |
CSIDL FONTS |
Виртуальная папка Шрифты |
CSIDL NETHOOD |
Папка, содержащая объекты сетевого окружения |
CSIDL_NETWORK |
Виртуальная папка Сетевое окружение (Network Neighborhood) |
CSIDL_PERSONAL |
Папка Мои документы |
CSIDL_PRINTERS |
Виртуальная папка Принтеры (Printers) |
CSIDL_PROGRAMS |
Папка Программы из главного меню, содержащая папки установленных на компьютере программ |
CSIDL RECENT |
Папка, содержащая ссылки на последние использовавшиеся документы (Recent) |
CSIDL SENDTO |
Папка, содержащая элементы контекстного меню Send To... |
CSIDL_STARTMENU |
Папка, содержащая элементы главного меню Пуск (Start) |
CSIDL_STARTUP |
Папка, содержащая элементы меню Автозапуск (Startup) |
CSIDL_TEMPIATES |
Папка, содержащая шаблоны типовых документов |
Третий вариант получить pidi нужной папки — интерактивный, с помощью функции Shell API.
function ShBrowseForFolder(var Ipbi: TBrowselnfo): PItemlDList;
Перед ее вызовом следует заполнить структуру типа TBrowselnfo, содержащую в частности pidi того элемента, который будет корневым. После вызова функции пользователь увидит перед собой диалоговое окно выбора папки (Рисунок 31.3).
Методы интерфейса IContextMenu
Таблица 31.3. Методы интерфейса IContextMenu
Метод |
Описание |
function QueryContextMenu (Menu: HMENU; indexMenu, idCmdFirst, idCmdLast, uFlags: UINT) : HResult; stdcall; |
Добавляет пункт к системному контекстному меню |
function InvokeCommand(var- Ipici: TCMInvokeCommandlnfo): HResult; stdcall; |
Осуществляет вызов обработчика |
function GetCommandString (idCmd, uType: UINT; pwReserved: POINT; PszName: LPSTR; cchMax: UINT) : HResult; stdcall; |
Возвращает описание добавленного пункта меню (подсказку или полное название) |
Рассмотрим их подробнее. Параметры метода QueryContextMenu означают следующее:
Menu — дескриптор системного меню; IndexMenu — позиция в меню, в которую следует вставить пункт (пункты); IdCmdFirst,IdCmdLast — диапазон допустимых значений для идентификаторов вставляемых пунктов меню; uFlags — набор флагов, главные из которых означают: CMF_NORMAL — обычный вызов контекстного меню, пункты могут быть добавлены. Значение этого флага нулевое, проверять его следует, очистив все биты в параметре uFlags, кроме пяти младших (маска $1F); CMF_DEFAULTONLY — устанавливается, если пользователь задал с объектом действие по умолчанию (например, двойной щелчок). В этом случае пункты меню добавляться не должны; CMF_VERBSONLY — устанавливается, если меню создается для ярлыка объекта, а не для самого объекта. В этом случае многие пункты меню создаваться не должны; CMF_EXPLORE — устанавливается, если меню создается для объекта, находящегося на левой панели Проводника.Для иллюстрации объектов — расширений контекстного меню — выберем пример ContMenu (поставляется с Delphi в папке DEMOS\ACTIVEX \SHELLEXT). В этом примере для объектов типа "проект Delphi" добавляется возможность запуска компилятора в командной строке. При вызове метода QueryContextMenu нужный пункт добавляется с помощью функции
InsertMenu!
function TContextMenu.QueryContextMenu(Menu: HMENU; indexMenu, idCmdFirst,
idCmdLast, uFlags: UINT): HResult;
begin
Result := 0; // или использовать MakeResult(SEVERITY_SUCCESS, // FACILITY_NULL, 0);
if ( (uFlags and $OOOOOOOF) = CMF__NORMAL)
or
((uFlags and CMF_EXPLORE) о 0) then begin
// Добавить один пункт меню во всплывающее меню
InsertMenu(Menu, indexMenu, MF__STRING or MF_BYPOSITION, idCmdFirst,
'Compile...');
Result := 1;
// или использовать MakeResult(SEVERITY_SUCCESS, //
FACILITY_NULL, 1)
end;
end;
Метод Getcornmandstring предоставляет системе данные о пункте меню, в частности, текст подсказки; эта подсказка будет отображаться в строке состояния Проводника, когда курсор находится в нужном месте меню.
Параметры Getcommandstring просты. Первый — idCmd — соответствует идентификатору пункта меню, второй — uType — запрос на тип информации (GCS_HELPTEXT — текст подсказки, GCS_VERB — полное название пункта меню). Наконец, параметры pszName и cchMax задают буфер, в который будут копироваться текстовые данные. Полное название необходимо системе, чтобы с его помощью вызывать предусмотренные в пункте действия программно. В примере ContMenu возврат названия (т. е. обработка запроса GCS_VERB) не предусмотрен, а в ответ на запрос GCS_HELPTEXT возвращается текстовая строка "Compile the selected Delphi project".
Наиболее сложным является метод Invokecommand. Он вызывается при выборе пользователем вставленного вами пункта меню. По сути дела метод InvokeCommand представляет собой прямой аналог обработчика onclick обычных пунктов меню (объектов TMenuitem) в Delphi.
Единственным параметром метода является структура типа TCMinvoke-commandinfo, поля которой имеют такое предназначение:
cbsize — размер структуры в байтах; hwnd — задает дескриптор окна, которое будет владельцем диалоговых окон, вызываемых из метода; fMask — определяет, заданы ли параметры dwHotkey/hicon; Ipverb — вызываемая команда; IpFarameters — параметры (если есть); IpDirectory — рабочая папка (поле не обязательно); nShow — флаг состояния окна, который будет передан в функцию ShowWindow (SW_*); dwHotKey — "горячая" комбинация клавиш, которая будет сопоставляться приложению, запускаемому из этого пункта меню (только если в параметре fMask установлен флаг CMIC_MASK_HOTKEY); hIсоn — значок, который будет сопоставляться приложению, запускаемому из этого пункта меню (только если в параметре fMask установлен флаг CMIC_MASK_ICON); Monitor — монитор по умолчанию (поле не обязательно).Отдельно следует остановиться на описании параметра ipverb. Как уже говорилось, он может представлять из себя как идентификатор пункта меню, так и его текст — строку, заканчивающуюся нулем. Чтобы выяснить это, нужно проверить старшее слово этого 32-разрядного параметра на равенство нулю. В примере ContMenu вызов по тексту не предусмотрен:
if (HiWord(Integer(Ipici.IpVerb)) <> 0) then
begin
Exit;
end;
Для создания расширения контекстного меню мы должны породить объект, поддерживающий эти интерфейсы. К сожалению, мастера, предусмотренные в Delphi, не позволяют в автоматизированном режиме создавать объекты, реализующие уже существующие интерфейсы. Поэтому и описание, и реализацию методов придется делать "по старинке", вручную. В примере ContMenu описание объекта таково:
TContextMenu = class(TComObject, IShellExtlnit, IContextMenu) private
FFileName: array[0..MAX_PATHj of Char;
protected
( IShellExtlnit }
function IShellExtlnit.Initialize = SEIInitialize;
function SEIInitialize(pidlFolder: PItemlDList; Ipdobj: IDataObject;
hKeyProgID: HKEY): HResult; stdcall; { IContextMenu }
function QueryContextMenufMenu: HMENU;
indexMenu, idCmdFirst, idCmdLast,
uFlags: UINT): HResult;
stdcall;
function InvokeCommand(var Ipici: TCMInvokeCommandlnfo): HResult; stdcall;
function GetCommandString(idCmd, uType: UINT; pwReserved: POINT;
pszName: LPSTR; cchMax: UINT): HResult;
stdcall;
end;
Вас может насторожить конструкция, описывающая переименование метода initialize интерфейса ishellExtinit. На самом деле одноименный метод имеется у объекта TComObject, и приведенный синтаксис как раз и предназначен для выхода из подобных ситуаций.
Последняя часть работы — регистрация созданного обработчика. Самое подходящее место для этого — метод updateRegistry фабрики класса. Разработчики примера ContMenu породили класс TContextMenuFactory, который при регистрации СОМ-сервера регистрирует создаваемые фабрикой объекты:
Classic := GUIDToString(Class_ContextMenu);
CreateRegKey('DelphiProjectXshellex', '', '')/'
CreateRegKey
('DelphiProject\shellex\ContextMenuHandlers', '', '');
CreateRegKey
('DelphiProject\shellex
\ContextMenuHandlers\ContMenu', '',
ClassID);
Пример ContMenu иллюстрирует "дельфийский" подход к созданию серверов СОМ через соответствующие объекты из иерархии объектов Delphi. Но в папке SHELLEXT вы найдете еще один пример создания расширения для контекстного меню, сделанный целиком и только с использованием интерфейсов и функций СОМ. Присмотритесь к этому примеру внимательнее, если хотите глубже понимать внутреннюю структуру СОМ-объектов.
Форма состояния процесса вывода отчета компонента TRvSystem
Рисунок 23.5. Форма состояния процесса вывода отчета компонента TRvSystem
В нем отображается информационная строка состояния, которая может быть настроена при помощи свойств объекта SystemFiler, представленного в компоненте TRvSystem одноименным свойством.
Вложенное свойство
property StatusFormat: string;
определяет строку форматирования для текста о состоянии процесса. Для нее предусмотрены следующие управляющие символы:
%с — текущее состояние процесса вывода; %р — номер текущей страницы; %f — номер первой страницы; %1 — номер последней страницы; %d — название устройства вывода (название принтера, имя файла, предварительный просмотр); %r — имя драйвера устройства вывода; %s — общее число страниц; %t — порт печати; %о-%9 — номера строк для свойства statusText (см. ниже).Вложенное свойство
property StatusText: TStrings;
позволяет задать до десяти строк (можно задать и больше, но они не будут восприняты строкой статуса) с какой-либо дополнительной информацией, описывающей процесс вывода. Первая строка списка будет выведена при наличии в свойстве statusFormat управляющего символа %0, вторая — при наличии символа %1 и т. д.
При помощи перечисленных свойств вы сможете детально описать процесс вывода отчета. В этом вам помогут методы-обработчики событий компонента TRvSystem.
До начата печати отчета и по его окончании (даже если печать была прервана) соответственно вызывается пара методов-обработчиков:
property OnBeforePrint: TNotifyEvent;
property OnAfterPrint: TNotifyEvent;
В начале печати непосредственно отчета (не заголовка) вызывается метод-обработчик
property OnPrint: TNotifyEvent;
Если вы печатаете одну страницу, будет вызван метод-обработчик
type
TPrintPageEvent = function(Sender: TObject;
var PageNum: Integer): Boolean;
property OnPrintPage: TPrintPageEvent;
Но до начала печати вызывается метод-обработчик
property OnNewPage: TNotifyEvent;
который обозначает генерацию страницы.
При печати колонтитулов в верхней и нижней частях страницы вызываются методы-обработчики
property OnPrintHeader: TNotifyEvent;
property OnPrintFooter: TNotifyEvent;
Разработчик может задать несколько опций для всего компонента TRvSystem, управляя тем самым процессом вывода отчета. Для это используется свойство
type
TSystemOption = (soUseFiler, soWaitForOK, soShowStatus, soAllowPrintFromPreview, soPreviewModal);
TSystemOptions = set of TSystemOption;
property SystemOptions: TSystemOptions;
Элементы типа TSystemOptions обозначают следующее:
soUseFiler — при установке этой опции в значение True вывод будет направляться в файл, заданный свойством SystemFiler, независимо от других настроек компонента; SoWaitForOK — если включить эту опцию, генерация отчета будет задержана до момента, когда пользователь нажмет кнопку ОК в диалоге настройки печати компонента (см. Рисунок 23.4); soshowstatus — эта опция управляет видимостью окна состояния процесса вывода отчета в компоненте; soAiiowPrintFromPreview — будучи включенной, эта опция позволяет печатать отчет из окна предварительного просмотра; soPreviewModal — при значении True делает окно предварительного просмотра модальным.
в качестве основного средства для
Генератор отчетов Rave Reports 5.0
Генератор отчетов Rave Reports 5.0 разработан фирмой Nevrona и входит в состав Delphi 7 в качестве основного средства для создания отчетов. Он состоит из трех частей:
ядро генератора отчетов обеспечивает управление отчетом и его предварительный просмотр, и отправку на печать. Исполняемый код ядра сервера включается в приложение Delphi, делая его полностью автономным при работе с отчетами на компьютере клиента; визуальная среда разработки отчетов Rave Reports предназначена для разработки самих отчетов. Она позволяет добавлять к отчету страницы, размешать на них графические и текстовые элементы управления, подключать к отчетам источники данных и т. д. Отчеты сохраняются в файлах с расширением rav и должны распространяться совместно с приложениями, использующими их; компоненты Rave Reports расположены на странице Rave Палитры компонентов Delphi. Они обеспечивают управление отчетами в приложении. Генератор отчетов устанавливается при инсталляции Delphi в папку \Delphi7\Rave5. Исходные коды компонентов разработчикам в Delphi недоступны.
Безусловно, визуальная среда разработки заметно упрощает процесс создания отчетов и позволяет добиться лучших результатов меньшими усилиями, чем в генераторе отчетов Quick Report, который использовался в предыдущих версиях Delphi. Тем не менее при первом знакомстве с продуктом заметны и его недостатки. Система помощи оставляет тягостное впечатление не только своей крайней лаконичностью, но и фактическими ошибками. Многие свойства и методы остались недокументированными, и наоборот — имеющиеся в статьях подсказки описания не имеют реальных аналогов в коде компонентов.
Однако будем надеяться, что недостатки будут со временем исправлены. А мы займемся детальным знакомством с процессом создания отчетов.
Компоненты Rave Reports и отчеты в приложении Delphi
ГЛАВА 23
Компоненты Rave Reports и отчеты в приложении Delphi
На первый взгляд кажется, что в сфере создания и печати отчетов в Delphi 7 произошла небольшая революция. Просматривая первый раз Палитру компонентов, вы не найдете в ней хорошо знакомой по прошлым версиям Delphi страницы QReport. Вместо старого генератора отчетов в состав Delphi 7 включен продукт Rave Reports 5.0 от фирмы Nevrona. "Ну и почему же это событие не дотягивает до революции в отчетах?" — спросит читатель. Авторы могут обосновать свою точку зрения.
Во-первых, компоненты QReport по-прежнему доступны разработчику — пакет DCLQRT70.BPL все так же занимает прочное место в папке \Delphi7\Bin и может быть установлен в Палитру компонентов обычным способом. Да и было бы странно ожидать другого от фирмы Borland, которая бдительно следит за обратной совместимостью приложений. Посмотрите к примеру на страницу Win 3.1 Палитры компонентов — новые поколения программистов никогда не видели "прабабушку" Windows XP, и все же исторические компоненты занимают свое исконное место!
Во-вторых, схема создания и внедрения отчетов в приложения Delphi практически не изменилась. В Rave Reports имеются и глобальный класс отчета, и классы полос, и компоненты преобразования данных. Существенным нововведением можно считать только визуальную среду создания отчетов, что несомненно облегчит жизнь создателей отчетов и сделает их работу эффективнее и приятнее.
Тем не менее, в Delphi 7 генератор отчетов Rave Reports является основным средством создания отчетов и его компоненты устанавливаются в Палитре компонентов по умолчанию на странице Rave. Поэтому главы этой части посвящены разнообразным аспектам разработки отчетов в Rave Reports.
В данной главе рассматриваются следующие вопросы:
какие компоненты входят в состав Rave Reports и на какие функциональные группы они делятся; что такое проект отчета и его структура; как включить отчет в состав приложения и какие компоненты для этого необходимы; компоненты управления отчетами.