Абстрактные, контролируемые инеконтролируемые виды
Как видно из дерева наследования на рис. 10.5, интерфейсные формы используются в проекте EmbeddedForms для создания двух категорий видов: неконтролируемы х (TValidView), для которых свойство Valid всегда равно True, и контролируемы х (TFickleView), для которых свойство Valid может изменять ся. Неконтролируемые виды можно использовать для Memo-полей с произвольным содержимым или, например, начальной или завершающей панелей мастера. Контролируемые виды должны применяться каждый раз, когда пользователь может ввести неверные данные, не подлежащие сохранению, — например, дату 31 февраля, расходы, превышающие общую сумму бюджета, и т. д. Поскольку в обоих случаях реализуется интерфейс IView, можно воспользоваться универсальным кодом для работы с обобщенным набором видов.
Рис. 10.5. Интерфейсные формы в проекте
Обе категории видов происходят от класса TAbstractView (листинг 10.4).
Листинг 10.4. Модуль VIEWS.PAS
unit Views; // Copyright © 1997 by Jon Shemitz, //all rights reserved. // Permission is hereby granted to //freely use, modify, and // distribute this source code PROVIDED //that all six lines of // this copyright and contact notice are //included without any // changes. Questions? Comments? //Offers of work? //mailto:jon@midnightbeach.com // ---------------------------------------------- // Отображает соглашение IView на внедренную //форму. Виды обычно // порождаются от TValidView или TFickleView. interface uses Models, Embedded; type TAbstractView = class(TEmbeddedForm, IView) procedure FormCreate(Sender: TObject); private fReadOnly: boolean; protected function GetValid: boolean; virtual; abstract; procedure SetValid(Value: boolean); virtual; abstract; function GetReadOnly: boolean; virtual; procedure SetReadOnly(Value: boolean); virtual; public procedure ReadFromModel(Model: TModel); virtual; procedure WriteToModel(Model: TModel); virtual; procedure AddNotifiee( Notify: IFrame); virtual; abstract; procedure RemoveNotifiee(Notify: IFrame); virtual; abstract; property Valid: boolean read GetValid write SetValid; property ReadOnly: boolean read fReadOnly write SetReadOnly; end; TViewClass = class of TAbstractView; implementation {$R *.DFM} function TAbstractView.GetReadOnly: boolean; begin Result := fReadOnly; end; // TAbstractView.GetReadOnly procedure TAbstractView.SetReadOnly(Value: boolean); begin fReadOnly := Value; Enabled := not Value; // Вид, доступный только для чтения, отображает // информацию, но не позволяет изменять ее; // вы можете переопределить SetReadOnly, // чтобы изменить визуальное представление таких видов. end; // TAbstractView.SetReadOnlyprocedure TAbstractView.ReadFromModel(Model: TModel); begin end; // TAbstractView.ReadFromModel procedure TAbstractView.WriteToModel(Model: TModel); begin end; // TAbstractView.WriteToModel procedure TAbstractView.FormCreate(Sender: TObject); begin inherited; _AddRef; // Чтобы Self можно было передавать // как интерфейсную ссылку end; end.
TAbstractView разделяет протокол IView на три части — доступность только для чтения, проверка корректности, обмен данными с моделью — и обрабатывает каждую часть отдельно:
реализация базовой функциональности Read-Only — пользователи не могут изменить данные на заблокированной форме, хотя на практике виды обычно переопределяют метод SetReadOnly, чтобы изменить визуальное представление видов, доступных только для чтения; | |
вся реализация проверки возлагается на потомков TValidView и TFickleView; | |
для ReadFromModel и WriteFromModel предоставляются фиктивные заглушки. Поскольку эти методы переопределяются в любом реальном объекте вида, желательно, чтобы виды всегда вызывали inherited. |
Как нетрудно догадаться по названию, предполагается, что вы не станете непосредственно использовать класс TAbstractView или напрямую наследовать от него. Вместо этого следует пользоваться TValidView и TFickleView.
Разумеется, все «абстрактные», «неконтролируемые» и «контролируемые» виды можно было свалить в единый класс TView. Разделение обладает двумя основными достоинствами: поскольку неконтролируемые виды игнорируют те части протокола IView, которые занимаются проверкой, программа работает немного быстрее и требует меньше памяти. Что еще важнее, при порождении конкретного вида от TValidView вместо TFickleView свойство Valid всегда остается равным True, даже если вы по неосторожности присвоите ему False (сравните листинги 10.5 и 10.6).
Листинг 10.5. Методы проверки корректности из модуля VALIDVIEWS.PAS
function TValidView.GetValid: boolean; begin Result := True; end; // TValidView.GetValid procedure TValidView.SetValid(Value: boolean); begin // TValidView всегда корректен - //игнорируем Value end; // TValidView.SetValid procedure TValidView.AddNotifiee(Notify: IFrame); begin // TValidView всегда корректен - игнорируем запрос на добавление end; // TValidView.AddNotifiee procedure TValidView.RemoveNotifiee(Notify: IFrame); begin // TValidView всегда корректен - игнорируем запрос на удаление end; // TValidView.RemoveNotifieeЛистинг 10.6. Фрагмент модуля FICKLEVIEW.PAS
type TFickleView = class(TAbstractView) private { Private declarations } fValid: boolean; fNotify: IFrame; // В данной реализации проверки //корректности поддерживается // всего один получатель уведомлений public { Public declarations } procedure AddNotifiee( Notify: IFrame); override; procedure RemoveNotifiee(Notify: IFrame); override; function GetValid: boolean; override; procedure SetValid(Value: boolean); override; end; implementation {$R *.DFM} procedure TFickleView.AddNotifiee(Notify: IFrame); begin fNotify := Notify; end; // TFickleView.AddNotifiee procedure TFickleView.RemoveNotifiee(Notify: IFrame); begin fNotify := Nil; end; // TFickleView.RemoveNotifiee function TFickleView.GetValid: boolean; begin Result := fValid; end; // TFickleView.GetValid procedure TFickleView.SetValid(Value: boolean); begin if Value <> fValid then begin fValid := Value; if Assigned(fNotify) then fNotify.OnValidChanged(Self, Self); end; // Value <> fValid end; // TFickleView.SetValidАрифметические функции и процедуры
Ceil Округление вверх
Floor Округление вниз
Frexp Вычисление мантиссы и порядка заданной величины
IntPower Возведение числа в целую степень. Если вы не собираетесь пользо-
ваться экспонентами с плавающей точкой, желательно исполь-
зовать эту функцию из-за ее скорости
Ldexp Умножение X на 2 в заданной степени
LnXP1 Вычисление натурального логарифма X+1. Рекомендуется для X,
близких к нулю
LogN Вычисление логарифма X по основанию N
Log10 Вычисление десятичного логарифма X
Log2 Вычисление двоичного логарифма X
Power Возведение числа в степень. Работает медленнее IntPower, но для
операций с плавающей точкой вполне приемлемо
Асинхронная пересылка файлов
Познакомившись с протоколом FTP в блокирующем (синхронном) режиме, кратко рассмотрим работу CsShopper в асинхронном режиме. Поскольку процесс регистрации на FTP-сервере подробно описан выше, наше основное внимание будет сосредоточено на пересылке, и особенно— на асинхронном приеме файла с FTP-сервера.
Перед тем как подключаться к FTP-серверу в асинхронном режиме, следует установить переключатель Asynchronous в групповом поле FTP Mode вкладки Options. Этот переключатель управляет режимом всего соединения; после того как SHOPPER32 подключится к FTP-серверу, групповое поле FTP Mode блокируется до окончания сеанса.
Процесс выбора принимаемого файла в асинхронном режиме происходит так же, как и в блокирующем режиме; другими словами, перед вызовом Retrieve мы присваиваем имя файла свойству Get. Отличия начинаются внутри Retrieve. Определив тип файла, мы присваиваем флагу состояния FFtpCmd значение FTP_TYPEI и тем самым приказываем серверу переслать файл как непрерывный поток байтов. Команда TYPE передается через процедуру SendFtpCmd.
Когда Winsock получает событие сокета FD_READ, которое происходит в результате ответа FTP-сервера на команду TYPE, он посылает процедуре FtpEvent сообщение с описанием события. В FtpEvent сообщение анализируется на предмет поиска событий FD_READ, FD_WRITE и FD_CLOSE. Для распознавания события сокета используется оператор case.
При получении события FD_READ процедура InfoEvent отправляет все содержимое буфера FRcvBuffer для вывода в приложении SHOPPER32. В буфере FRcv Buffer, содержащем код ответа от сервера, ищется символ 4 или 5, свидетель ствующий об ошибке FTP. Если поиск окажется успешным, FFtpCmd присваивается значение FTP_FAIL, которое сигнализирует приложению о возникнове нии ошибки.
В противном случае процедура ProcessRecvData обрабатывает FRcvBuffer и флаг состояния FFtpCmd с использованием оператора case. Так как FFtpCmd имеет значение FTP_TYPEI, ProcessRecvData вызывает процедуру ProcessTypeI, в которой выполняется подробный анализ содержимого FRcvBuffer. Следующий фрагмент кода показывает, как это делается:
procedure TCsShopper.ProcessTypeI; begin case GetReplyCode(FRcvBuffer) of 200 : begin if Pos('200-',String(FRcvBuffer)) = 0 then // Сервер ждет, пока мы создадим // соединение данных и пошлем команду USER begin ProcessPort; end; { остаток кода пропущен } end; // case FillChar(FRcvBuffer, SizeOf(FRcvBuffer),#0); end;Если код ответа равен 200, вызывается процедура ProcessPort, из которой в свою очередь вызывается InitDataConn, выполняющая четыре задачи:
создание сокета для соединения данных; | |
вызов WSAAsyncSelect для создания логического номера окна, позволяю щего FtpDataEvent перехватывать события сокета, связанные с соедине нием данных; | |
вызов функции Winsock API bind для связывания нового сокета данных; | |
вызов listen для перевода сокета данных в состояние «прослушивания» (listening). |
Если в результате вызова InitDataConn будет создан допустимый сокет данных, ProcessPort создает для соединения данных уникальный номер порта, который затем передается процедурой SendFtpCmd. Наконец, флагу состояния FFtpCmd присваивается значение FTP_RETR, которое сигнализирует CsShopper о том, что следующее событие сокета FD_READ должно анализироваться в контекс те приема файла.
Когда на управляющем соединении происходит следующее событие FD_READ (при условии отсутствия ошибок сокета или отрицательных кодов ответа), вызывается процедура ProcessRecvData, которая в свою очередь инициирует ProcessGet.
В ProcessGet при получении кода ответа 200 (признак успеха) создается локальный файл, имя которого совпадает с именем файла на сервере. В дальнейшем код ответа 150 сигнализирует FTP-клиенту о том, что сервер приступил к пересылке информации через соединение данных.
Сразу же после того, как FTP-сервер свяжется с клиентом через соединение данных, Winsock уведомляет об этом процедуру FtpDataEvent с помощью события FD_ACCEPT. В ветви FD_ACCEPT оператора case вызывается функция WSAAsyncSelect, которая инициализирует сокет данных для приема только следующих событий: FD_READ, FD_WRITE и FD_CLOSE. Следующий фрагмент процедуры FtpDataEvent показывает, как это делается:
FD_ACCEPT : begin FStartTime := GetTickCount; FIntTime := FStartTime; if FListenSocket <> INVALID_SOCKET then begin nLen := SizeOf(TSockAddr); FDataSocket := accept(FListenSocket, @FRemoteHost, @nLen); if FDataSocket = SOCKET_ERROR then begin InfoEvent(Concat('Error : ',WSAErrorMsg)); FFtpCmd := FTP_FAIL; Exit; end; nStat := WSAAsyncSelect(FDataSocket, FDataWnd, DATA_EVENT, FD_READ or FD_WRITE or FD_CLOSE); if nStat = SOCKET_ERROR then begin InfoEvent(Concat('Error : ',WSAErrorMsg)); FFtpCmd := FTP_FAIL; Exit; end; { остаток кода пропущен } end; end;При приеме первого и последнего пакета данных через соединение данных Winsock уведомляет FtpDataEvent с помощью события FD_READ, что приводит к вызову RecvData для получения и сохранения поступающих данных в локальном файле. После завершения пересылки FTP-сервер закрывает соединение данных со своей стороны, заставляя Winsock послать сообщение FD_CLOSE. На этом пересылку файла логично было бы завершить, но иногда в сокете данных FTP-клиента все еще остаются непрочитанные данные. Чтобы избежать потерь информации, мы присваиваем флагу FTransferDone значение TRUE. Все сказанное демонстрируется следующим фрагментом кода из процедуры FtpDataEvent:
FD_CLOSE : begin FTransferDone := TRUE; case FFTPCmd of FTP_RETR, FTP_LIST, FTP_VIEW : RecvData; FTP_STOR : SendData; end; end;Флаг FTransferDone сообщает о необходимости продолжить чтение оставшихся данных сокета в цикле while, как показано в следующем фрагменте кода процедуры RecvData:
FTP_RETR : begin { часть кода пропущена } if FTransferDone then // Работа с //FTP-сервером закончена, // однако необходимо прочитать // и сохранить данные, оставшиеся // в сокете данных begin Done := FALSE; while not Done do begin BlockWrite(FRetrFile, FDataBuffer, Response); { часть кода пропущена } Response := recv(FDataSocket, FDataBuffer, SizeOf(FDataBuffer), 0); if Response = SOCKET_ERROR then begin Done := TRUE; WSAAsyncSelect(FDataSocket, // Прекратить посылку FDataWnd, 0, 0); // уведомлений CloseSocket(FDataSocket); System.CloseFile(FRetrFile); ChangeBusy(FALSE); ChangeDataDone(TRUE); InfoEvent(Concat('ERROR : ',WSAErrorMsg)); end; if Response = 0 then // Данных не осталось begin { часть кода пропущена } Done := TRUE; WSAAsyncSelect(FDataSocket, FDataWnd, 0, 0); CloseSocket(FDataSocket); System.CloseFile(FRetrFile); ChangeBusy(FALSE); ChangeDataDone(TRUE); GetList; end; end; end else if Response > 0 then // FTP-сервер продолжает // посылать данные, // их необходимо обработать begin BlockWrite(FRetrFile, FDataBuffer, Response); { часть кода пропущена } end; end;Передача файла FTP-серверу в асинхронном режиме выполняется по тому же принципу, что и прием.
Асинхронное получение адреса
Блокирующие функции gethostbyname и gethostbyaddr используются достаточно просто. С асинхронными версиями этих функций, WSAAsyncGetHostByName и WSA AsyncGetHostByAddr, дело обстоит несколько сложнее. Чтобы понять, как работает асинхронный процесс, мы посмотрим, как WSAAsyncGetHostByName вызывается в программе RESOLVER32.
Прежде всего смените значение свойства Access с Blocking на NonBlocking — для этого следует установить переключатель NonBlocking в групповом поле TypeOfLookup (см. рис. 5.6). При нажатии кнопки Resolve имя передается свойству HostName.
Рис. 5.6. Переход от блокирующих функций к псевдоблокирующим
Поскольку FAsync имеет значение NonBlocking, SetRemoteHostName передает его процедуре SetAsyncHostName (см. листинг 5.9).
Листинг 5.9. Метод TCsSocket.SetAsyncHostName — преобразование имени хоста
procedure TCsSocket.SetAsyncHostName (ReqdHostName : String); var IPAddress : TInaddr; SAddress: array[0..31] of char; begin FillChar(FAsyncBuff, SizeOf(FAsyncBuff), #0); FAsyncRemoteName := ReqdHostName; StrPcopy(SAddress, FAsyncRemoteName); IPAddress.s_addr := inet_addr(SAddress); if IPAddress.s_addr <> INADDR_NONE then { Это IP-адрес } begin FAddress := IPAddr; FAsyncType := AsyncAddr; if IPAddress.s_addr <> 0 then FTaskHandle := WSAAsyncGetHostByAddr(FAsyncHWND, ASYNC_EVENT, pChar(@IPAddress), 4, PF_INET, @FAsyncBuff[0], SizeOf(FAsyncBuff)); if FTaskHandle = 0 then begin if FNoOfBlockingTasks > 0 then dec(FNoOfBlockingTasks); FStatus := Failure; ErrorEvent(FStatus,WSAErrorMsg); if FOKToDisplayErrors then raise ECsSocketError.create(WSAErrorMsg); Exit; end else FStatus := Success; end else { Нет, это больше похоже на символьное имя хоста } begin FAddress := HostAddr; FAsyncType := AsyncName; Inc(FNoOfBlockingTasks); FTaskHandle := WSAAsyncGetHostByName (FAsyncHWND, ASYNC_EVENT, @FpHostName[0], @FAsyncBuff[0], MAXGETHOSTSTRUCT); if FTaskHandle = 0 then begin FStatus := Failure; if FNoOfBlockingTasks > 0 then dec(FNoOfBlockingTasks); ErrorEvent(FStatus,WSAErrorMsg); if FOKToDisplayErrors then raise ECsSocketError.create(WSAErrorMsg); Exit; end else FStatus := Success; end; end;SetAsyncHostName вызывает процедуру WSAAsyncGetHostByName с пятью важными аргументами. FASyncHWND — логический номер окна, которому асинхронная функция должна отправить сообщение о завершении операции просмотра. Он инициализируется в конструкторе TCsSocket.Create вызовом AllocateHWND с параметром-процедурой AsyncOperation. ASYNC_EVENT — константа события, используемая в WSAAsyncGetHostByName. Символьный массив FAsyncBuff содержит результат выполнения операции. Наконец, MAXGETHOSTSTRUCT — константа Winsock, определяющая максимальный размер буфера FAsyncBuff. Процедура WSAAsyncGet HostByName возвращает номер задачи в виде значения типа TaskHandle, которое затем присваивается полю FTaskHandle.
WSAAsyncGetHostByName немедленно завершает работу с нулевым кодом, если вызов был неудачным; в случае удачного вызова она возвращает положительное число. Тем не менее отличное от 0 значение FTaskHandle свидетель ствует лишь об успешном вызове WSAAsyncGetHostByName, но не гарантирует успех последующей операции просмотра (которая продолжает выполняться в фоновом режиме).
После завершения просмотра Winsock DLL инициирует событие ASYNC_EVENT, сообщая процедуре AsyncOperation о том, что она должна обработать сообщение ASYNC_EVENT (см. листинг 5.10).
Листинг 5.10. Процедура AsyncOperation
procedure TCsSocket.AsyncOperation(var Mess : TMessage); var MsgErr : Word; begin if Mess.Msg = ASYNC_EVENT then begin MsgErr := WSAGetAsyncError(Mess.lparam); if MsgErr <> 0 then begin FStatus := Failure; ErrorEvent(FStatus,WSAErrorMsg); if FOKToDisplayErrors then raise ECsSocketError.create(WSAErrorMsg); Exit; end else begin FStatus := Success; InfoEvent('WSAAsync operation succeeded!'); case FAsyncType of AsyncName, AsyncAddr : begin FHost := pHostent(@FAsyncBuff); if (FHost^.h_name = NIL) then begin { Неизвестный хост, отменяем попытку... } FStatus := Failure; if FAsyncType = AsyncName then LookUpEvent(resIPAddress,'',FALSE) else LookUpEvent(resHostName,'',FALSE); if FOKToDisplayErrors then raise ECsSocketError.create ('Unable to resolve host'); Exit; end; if length(StrPas(FHost^.h_name)) = 0 then begin InfoEvent('Host lookup failed!'); FStatus := Failure; if FAsyncType = AsyncName then LookUpEvent(resIPAddress,'',FALSE) else LookUpEvent(resHostName,'',FALSE); if FOKToDisplayErrors then raise ECsSocketError.create ('Unknown host'); Exit; end; case FAddress of IPAddr : begin Move(FHost^.h_addr_list^, Fh_addr, SizeOf(FHost^.h_addr_list^)); FAsyncRemoteName := StrPas(FHost^.h_name); LookUpEvent(resHostName, FAsyncRemoteName, TRUE); end; HostAddr : begin Move(FHost^.h_addr_list^, Fh_addr, SizeOf(FHost^.h_addr_list^)); SetUpAddress; FAsyncRemoteName:= StrPas(inet_ntoa(FSockAddress. sin_addr)); LookUpEvent(resIPAddress,FAsyncRemoteName, TRUE); end; end;{case} end; AsyncServ : begin FServ := pServent(@FAsyncBuff); if FServ^.s_name = NIL then begin { Сервис недоступен } FStatus := Failure; LookUpEvent(resService,'',FALSE); if FOKToDisplayErrors then raise ECsSocketError.create(WSAErrorMsg); Exit; end; FAsyncPort := IntToStr(ntohs(FServ^.s_port)); LookUpEvent(resService, FAsyncPort, TRUE); end; AsyncPort : begin FServ := pServent(@FAsyncBuff); if FServ^.s_name = NIL then begin { Сервис недоступен } FStatus := Failure; LookUpEvent(resPort,'',FALSE); if FOKToDisplayErrors then raise ECsSocketError.create(WSAErrorMsg); Exit; end; FAsyncService := StrPas(FServ^.s_name); LookUpEvent(resPort, FAsyncService, TRUE); end; AsyncProtoName : begin FProto := pProtoEnt(@FAsyncBuff); if FProto^.p_name = NIL then begin FStatus := Failure; LookUpEvent(resProto,'',FALSE); if FOKToDisplayErrors then raise ECsSocketError.create(WSAErrorMsg); Exit; end; FAsyncProtoNo := IntToStr(FProto^.p_proto); LookUpEvent(resProto, FAsyncProtoNo, TRUE); end; AsyncProtoNumber : begin FProto := pProtoEnt(@FAsyncBuff); if FProto^.p_name = NIL then begin FStatus := Failure; LookUpEvent(resProtoNo,'',FALSE); if FOKToDisplayErrors then raise ECsSocketError.create(WSAErrorMsg); Exit; end; FAsyncProtocol := StrPas(FProto^.p_name); LookUpEvent(resProtoNo, FAsyncProtocol, TRUE); end; end; if FNoOfBlockingTasks > 0 then dec(FNoOfBlockingTasks); end; end; end;Функция WSAGetAsyncError проверяет значение переменной Mess. Если переменная сообщает о происшедшей ошибке, AsyncOperation вызывает ErrorEvent для вывода причины ошибки из WSAErrorMsg, а затем завершает работу, присваивая флагу FStatus значение Failure. Если ошибки не было, мы анализируем переменную FAsyncType.
При вызове WSAAsyncGetHostByName мы присваиваем FAsyncType значение AsyncName, чтобы установить признак асинхронного поиска имени. Затем оператор case переходит к фрагменту, соответствующему значению AsyncName. Здесь символьный массив FAsyncBuff, содержащий результаты поиска, преобразуется в структуру pHostent и сохраняется в поле FHost. SetUpAddress читает адресную структуру найденного хоста и получает искомый IP-адрес. Наконец, процедура LookUpEvent возвращает IP-адрес программе RESOLVER32.
Базовая программа-фильтр
Как я упоминал в начале этой главы, программы-фильтры обычно получают командную строку с параметрами и именами входных/выходных файлов, обрабатывают входную информацию в соответствии с полученными параметрами и создают выходной файл.
Столь общее описание оставляет более чем достаточно возможностей для импровизации. Например, программа для подсчета строк может получать имена сразу нескольких файлов (в том числе и файловые маски), а при указании некоторого параметра- считать не только текстовые строки, но также слова и символы или даже выдавать распределение слов и символов по относительной частоте. В более сложной программе результат работы может представлять собой отдельный файл, полученный преобразованием одного или нескольких входных файлов, или сразу несколько файлов, полученных в результате обработки одного входного файла.
Несмотря на все различия в сложности, фильтры обладают рядом общих функций. Все они обрабатывают содержимое командной строки, читают входные файлы и записывают выходные. Разные программы существенно отличаются друг от друга лишь промежуточной стадией обработки. Благодаря этой общности можно создать группу функций, которые реализуют основные задачи фильтров и позволяют быстро создавать нестандартные фильтры, для чего потребуется лишь указать синтаксис командной строки и написать код для стадии «обработки». Ввод, вывод, анализ командной строки - все это уже присутствует. Программа-фильтр хранится в виде концентрата, остается лишь добавить воду... то есть обработку.
Благодарности
Благодарю Мардж Макрей (Marge McRae), друга и отличного соседа, за первое чтение рукописи и за предложенную идею с персонажем Мардж Рейнольдс (которая, кстати, не имеет ни малейшего отношения к настоящей Мардж).
Дон Тейлор
Выпуск такой книги требует неимоверных усилий- особенно когда в ее написании участвуют столько авторов (причем один из них нередко запаздывает со сдачей материалов). Дениза Константин (Denise Constantine), наш редактор проекта, проделала огромную работу. Ей удалось направить проект по правильному пути и разобраться с бесчисленными мелочами, мешающими выпуску книги. Спасибо Денизе - она заставила-таки меня сдавать работу в срок.
Джим Мишель
Хочу поблагодарить Джеффа Дантеманна (Jeff Duntemann) за то, что он вывел мою писательскую карьеру на орбиту успеха.
Джон Пенман
Хочу поблагодарить мою жену Таню, которая давно примирилась с выпавшим на ее долю тяжким жребием.
Джон Шемитц
Целостность структуры
и циклические ссылки
По иронии судьбы рекурсивная иерархия в одной таблице заметно упрощает обеспечение целостности структуры : одно поле таблицы ссылается на другое, принадлежащее этой же таблице. В пределах одной таблицы каждое значение Boss_ID равно значению Emp_ID другой записи или nil (для объектов верхнего уровня). При этом защищаются все потомки объекта — значение Emp_ID нельзя изменять, если от него зависят другие записи. Если же объединяющие значения находятся в нескольких полях или таблицах, в результате чего становится возможной многоуровневая группировка или установка сложных связей, обеспечить целостность структуры будет сложнее.
Для программы, работающей с иерархией, наибольшую опасность представляют циклические ссылки. Если объект ссылается на несуществующего родителя, проблему можно заметить и исправить. Но, если родитель объекта оказывается одновременно и его потомком (если объекты разделены несколькими промежуточными поколениями, такую ситуацию будет нелегко обнаружить), программа зацикливается.
Где же выход? Можно проверять каждого «кандидата в предки» и смотреть, не присутствуют ли какие-либо из его предков в текущем «семействе» (правда, это будет довольно накладно с точки зрения производительности). Кроме того, в программу можно вставить счетчик-предохранитель, который инициирует исключение после определенного количества циклов поиска. Одно из преимуществ графических иерархических элементов как раз и заключается в том, что пользователь просто не сможет создать циклическую ссылку, так как это противоречит логике работы с элементом.
Читаем, чтобы записывать?
На самом деле происходит следующее: в большинстве случаев действительно применима простая модель, описанная выше. Однако, если свойство является потомком TPersistent (например, TBitmap или TFont), происходит нечто странное. Для потомков TPersistent метод write вызывается в тех случаях, когда свойство задается в режиме конструирования или изменяется в режиме выполнения— но не при создании и загрузке компонента из DFM-потока его формы. Вместо этого runtime-библиотека вызывает метод read данного свойства, чтобы получить указатель на присвоенный ему private-объект, а затем использует полученный указатель для вызова метода чтения из потока. То есть при загрузке компонента метод write не вызывается!
Разумеется, в большинстве случаев это несущественно — свойство все равно загружается и получает в режиме выполнения то же значение, что было задано в режиме конструирования. Тем не менее в некоторых ситуациях это все же может отразиться на вашей программе.
Во-первых, метод read никогда не должен возвращать Nil. Мысль о том, чтобы отложить создание private-объекта до того момента, когда метод write предоставит копируемое значение, выглядит вполне разумно. К сожалению, код загрузки компонентов Delphi недостаточно умен — он просто не замечает, что у него нет объекта TPersistent, которому нужно дать команду загрузиться из потока. Поэтому если метод read возвращает Nil, то при загрузке компонента происходит GPF (General Protection Fault, ошибка защиты). Кстати, именно это обстоятельство привлекло мое внимание, хотя признаюсь, что я не сразу разобрался в сути происходящего.
Во-вторых, не стоит использовать метод write для того, чтобы извлекать информацию из private-объекта свойства и сохранять ее в других runtime-полях вашего компонента. Метод write вызывается при непосредственном задании свойства в режиме конструирования или выполнения, но не при косвенном задании этого свойства, происходящем в момент загрузки компонента. Если воспользоваться методом write для обновления внутреннего состояния компонента, загрузка будет работать неверно.
Что дальше?
Итак, я описал еще один способ получения перетаскиваемых файлов. В большинстве случаев он способен полностью заменить код, предложенный в предыдущей главе. Но важнее другое: мы взяли хорошо знакомый процесс (прием файлов) и реализовали его на основе совершенно новой (для нас) технологии COM/OLE. Заодно мы узнали, как OLE используется в программах. Теперь на основе полученных знаний мы создадим нечто совершенное иное, новое и гораздо более сложное— сервер (то есть источник) перетаскивания.
Что делать с кодом Windows?
Правильный ответ— инкапсулировать. Именно это делает Delphi, и делает очень успешно. Идея Delphi заключается как раз в том, чтобы оградить вас от мелких неприятных деталей Windows-программирования, чтобы все усилия можно было сосредоточить на смысловой части приложения. То же самое мы проделаем и с FMDD — «упакуем» его в одноименный модуль Delphi.
Вместо того чтобы заставлять форму возиться с обработкой WM_DROPFILES, мы определим в модуле FMDD специальную функцию, с помощью которой обработчик OnMessage формы сможет получить объект с полными сведениями о происходящем перетаскивании. Этот объект будет содержать всю информацию, полученную от интерфейса FMDD Windows, но объединенную в простую и удобную структуру:
TDragDropInfo = class (TObject) private FNumFiles : UINT; FInClientArea : Boolean; FDropPoint : TPoint; FFileList : TStringList; public constructor Create (ANumFiles : UINT); destructor Destroy; override; property NumFiles : UINT read FNumFiles; property InClientArea : Boolean read FInClientArea; property DropPoint : TPoint read FDropPoint; property Files : TStringList read FFileList; end;Помимо структуры TDragDrop, в модуле FMDD определены три функции: AcceptDroppedFiles, UnacceptDroppedFiles и GetDroppedFiles. Две первые инкапсулируют функцию DragAcceptFiles, а третья вызывается при получении сообщения WM_DROPFILES и возвращает объект TDragDropInfo. В листинге 3.3 содержится первая версия модуля, FMDD1.PAS.
Листинг 3.3. Первая версия модуля FMDD, инкапсулирующего
интерфейс перетаскивания
{
FMDD1.PAS — Первая версия модуля, инкапсулирующего перетаскивание
файлов из File Manager
Автор: Джим Мишель
Дата последней редакции: 27/04/97
} unit fmdd1; interface uses Windows, Classes; type TDragDropInfo = class (TObject) private FNumFiles : UINT; FInClientArea : Boolean; FDropPoint : TPoint; FFileList : TStringList; public constructor Create (ANumFiles : UINT); destructor Destroy; override; property NumFiles : UINT read FNumFiles; property InClientArea : Boolean read FInClientArea; property DropPoint : TPoint read FDropPoint; property Files : TStringList read FFileList; end; function GetDroppedFiles (hDrop : THandle) : TDragDropInfo; procedure AcceptDroppedFiles (Handle : HWND); procedure UnacceptDroppedFiles (Handle : HWND); implementation uses ShellAPI; constructor TDragDropInfo.Create (ANumFiles : UINT); begin inherited Create; FNumFiles := ANumFiles; FFileList := TStringList.Create; end; destructor TDragDropInfo.Destroy; begin FFileList.Free; inherited Destroy; end; function GetDroppedFiles (hDrop : THandle) : TDragDropInfo; var DragDropInfo : TDragDropInfo; TotalNumberOfFiles, nFileLength : Integer; pszFileName : PChar; i : Integer; begin { hDrop - логический номер внутренней структуры данных Windows с информацией о перетаскиваемых файлах. } { Определяем общее количество брошенных файлов, передавая функции DragQueryFile индексный параметр -1 } TotalNumberOfFiles := DragQueryFile (hDrop , $FFFFFFFF, Nil, 0); DragDropInfo := TDragDropInfo.Create (TotalNumberOfFiles); { Проверяем, были ли файлы брошены в клиентской области } DragDropInfo.FInClientArea := DragQueryPoint (hDrop, DragDropInfo.FDropPoint); for i := 0 to TotalNumberOfFiles - 1 do begin { Определяем длину имени файла, сообщая DragQueryFile о том, какой файл нас интересует ( i ) и передавая Nil вместо длины буфера. Возвращаемое значение равно длине имени файла. } nFileLength := DragQueryFile (hDrop, i , Nil, 0) + 1; GetMem (pszFileName, nFileLength); { Копируем имя файла — сообщаем DragQueryFile о том, какой файл нас интересует ( i ), и передаем длину буфера. ЗАМЕЧАНИЕ: Проследите за тем, чтобы размер буфера на 1 байт превышал длину имени, чтобы выделить место для завершающего строку нулевого символа! } DragQueryFile (hDrop , i, pszFileName, nFileLength); { Заносим файл в список } DragDropInfo.FFileList.Add (pszFileName); { Освобождаем выделенную память... } FreeMem (pszFileName, nFileLength); end; { Вызываем DragFinish, чтобы освободить память, выделенную Shell для данного логического номера. ЗАМЕЧАНИЕ: Об этом шаге нередко забывают, в результате возникает утечка памяти, а программа начинает медленнее работать. } DragFinish (hDrop); Result := DragDropInfo; end; procedure AcceptDroppedFiles (Handle : HWND); begin DragAcceptFiles (Handle, True); end; procedure UnacceptDroppedFiles (Handle : HWND); begin DragAcceptFiles (Handle, False); end; end.Чтобы старая тестовая программа работала с новым интерфейсом, в нее придется внести ряд изменений. Во-первых, замените ссылку на модуль ShellAPI в секции uses ссылкой на FMDD1. Затем исправьте обработ чики событий формы в соответствии с листингом 3.4. Обновленная версия программы содержится в файлах DRAG2.DPR и DRAGFRM2.PAS на прилагаемом компакт-диске.
Листинг 3.4. Использование нового интерфейса для перетаскивания
файлов из File Manager
По-моему, новым интерфейсом пользоваться намного проще. В полном соответствии с духом Delphi мы убрали код для работы с Windows API из приложения и вынесли его с глаз долой в отдельный модуль. Модуль FMDD копается во внутренностях Windows и достает оттуда нужный объект, с которым мы умеем работать. В результате код получается компактным и понятным, более простым в написании и сопровождении.
Что такое DLL и зачем они нужны?
DLL (Dynamic Link Library, библиотека динамической компоновки)— разновидность выполняемых файлов Windows, в которых содержится код или данные, используемые другими программами. По своей концепции DLL напоминают модули Delphi, они тоже представляют собой «упакованные» фрагменты кода, с помощью которых ваша программа может выполнять различные действия. Концепция похожа — но с ее реализацией дело обстоит совершенно иначе.
Компоновка модулей Delphi выполняется статически. Это означает, что во время компиляции копия кода всех модулей, используемых вашей программой, помещается в EXE-файл. Каждая программа, использующая тот или иной модуль, содержит отдельную копию этого модуля в своем EXE-файле. Обычно это не так уж плохо — программы должны быть по возможности самостоятельными. Тем не менее есть как минимум две веские причины, по которым статическая компоновка иногда нежелательна.
Если у вас имеется большой модуль, который используется многими программами, ваши программы будут содержать большое количество повторяющегося кода. Хотя дисковое пространство сейчас обходится примерно в 30 центов за мегабайт и проблема стала не столь актуальной, как раньше (здесь мы не будем обращать внимания на минимальный размер сектора), что произойдет, если вам потребуется запустить четыре или пять таких программ одновременно? В итоге код модуля будет дублироваться в памяти. Память тоже не так уж дорога, но и дешевой ее не назовешь — во всяком случае настолько дешевой, чтобы расходовать ее понапрасну.
Вторая причина, по которой статическая компоновка может оказаться нежелательной, — гибкость. Предположим, вы только что написали новейший текстовый редактор, настоящее программное чудо, и теперь хотите научить его импортировать документы из других файловых форматов (это необходимо сделать, чтобы выдержать конкуренцию на рынке текстовых редакторов). Конечно, можно написать специальный модуль для каждого распространенного файлового формата и выбросить продукт на рынок. Но через полгода выходит новая версия какого-нибудь Word Grinder Max (надеюсь, продукта с таким названием в действительности не существует) с новым форматом, и ваша программа устаревает! Единственный способ выйти из положения и научить программу работать с новым форматом — выпустить обновление, на котором вы не заработаете ничего, кроме хлопот. Кроме того, снова возникает проблема размера. При статической компоновке кода для работы с сотнями разных форматов ваша программа будет перегружена огромным количеством балласта — кода, который использу ется очень редко или нужен очень узкому кругу клиентов.
Обе проблемы решаются с помощью динамической компоновки. Вместо того чтобы копировать код модуля в EXE-файл приложения, DLL позволяет вынести многократно используемый код в специальный библиотечный файл, который будет загружаться во время выполнения только при необходимости. Даже если пять разных программ будут пользоваться функциями из DLL, на диске (и, что еще важнее, в памяти) будет храниться всего одна копия кода. В EXE-файл включается не статический фрагмент кода, а лишь инструкция насчет того, где программа должна искать необходимый код. Значит, вам уже не придется набивать свой текстовый редактор бесчисленными функциями для преобразования формата, достаточно предусмот реть возможность подключения новых DLL. Поддержка нового формата сводится, таким образом, к написанию DLL и распространению ее среди тех пользователей, которым это потребуется.
Вот что является подлинной гибкостью.
Что такое OLE?
Термин OLE— сокращение от «Object Linking and Embedding», то есть «связывание и внедрение объектов». С помощью этой технологии ваши приложения могут обмениваться информацией с другими приложениями через стандартные интерфейсы, доступ к которым возможен из множества различных языков программирования. Например, через интерфейс OLE программа Delphi может управлять работой Microsoft Word и заставлять его выполнять любые действия — загружать и печатать файлы, автоматически создавать документы и т. д. В документации Windows это называется «OLE Automation». С помощью OLE также создаются расширения для оболочки Windows 95, файловые ссылки, ярлыки (shortcuts) и вообще почти все, с помощью чего две программы в наши дни могут общаться друг с другом.
За те несколько лет, что прошли с момента выхода первой версии, технология OLE несколько раз подвергалась усовершенствованиям и переимено ваниям. Кроме термина «OLE» использовались термин «OCX» и с недавних пор — «ActiveX». Эта технология, как бы ее ни называли, построена на основе спецификации COM (Component Object Model, многокомпонентная модель объекта), которая и представляет в данном случае наибольший интерес. COM — это просто способ определения интерфейса, который полностью скрывает его реализацию. Спецификация интерфейса COM похожа на интерфейс ную часть модуля Delphi — вы знаете, что делает интерфейс, но не видите, как он это делает.
OLE в Windows — всего лишь набор частично реализованных спецификаций COM. Это нужно твердо усвоить. Например, интерфейс перетаскивания состоит из четырех основных интерфейсов: IDropTarget, IDropSource, IDataObject и IEnumFormatEtc. Но ни один из этих интерфейсов не реализован! Существуют функции, которые вызывают эти интерфейсы, однако вы сами должны написать код, который реализует эти интерфейсы и возвращает функциям Windows необходимые данные. OLE лишь определяет общие контуры — а вся грязная работа по их заполнению достается вам.
Что такое Winsock?
Winsock — сокращение от «Windows Sockets», это интерфейсная прослойка между Windows-приложением и базовой сетью TCP/IP. Интерфейс сокетов впервые появился в Berkeley Unix как API для работы с сетями TCP/IP. Winsock базируется на Berkeley Sockets API и включает большую часть стандартных функций BSD API, а также некоторые расширения, специфические для Windows. Поддержка сетевого взаимодействия через TCP/IP в Windows-программе сводится к вызову функций Winsock API и использованию библиоте ки WINSOCK.DLL, реализующей интерфейс Winsock.
Программисту на Delphi проще всего работать с Winsock API с помощью компонентов. В этой главе мы создадим компонент CsSocket, инкапсулирую щий Winsock API. Он обладает несколькими несомненными достоинствами:
API становится составной частью Delphi VCL; | |
инкапсуляция облегчает многократное использование кода; | |
приложение-клиент видит четкий интерфейс, работа с которым происходит через свойства и методы. |
Несомненно, компонент CsSocket удобен для программирования на Delphi, но он не претендует на полноту. На фундаменте CsSocket вы сможете построить дочерние компоненты, предназначенные для работы с любым специали зированным Internet-протоколом. Компонент Winsock, поддерживающий все известные Internet-протоколы, получился бы слишком сложным и громоздким. Вместо этого мы воспользуемся CsSocket как основой для создания новых компонентов, работающих с конкретными протоколами.
Например, компонент для работы с гипертекстовым протоколом (HTTP) создается так:
Создайте новый компонент, производный от CsSocket. В конструкторе нового компонента задайте свойству Service значение HTTP. Добавьте методы и свойства, необходимые для работы с HTTP.В следующей главе мы посмотрим, как это делается, на примере компонента для клиентского приложения FTP.
CsKeeper за работой
Приложение KEEPER32 (находится на CD-ROM в каталоге этой главы) показывает, как компонент CsKeeper используется в приложении. Форма приложения содержит три элемента-вкладки (TabSheet). Вся основная работа выполняется на первой вкладке, tsKeeper (см. рис. 7.1). Также присутствуют вкладки tsOptions и tsAbout (о них будет рассказано ниже).
Рис. 7.1. KEEPER32 в режиме конструирования (отображается вкладка tsKeeper)
Но перед тем, как запускать приложение KEEPER32, необходимо выполнить некоторые подготовительные действия. Конечно, можно определить поведение компонента CsKeeper1, изменяя значения его свойств в инспекторе объектов в режиме конструирования (см. рис. 7.2).
Однако работа со свойствами в режиме конструирования удобна для разработчика приложения, но никак не для пользователя — например FTP-администратора, который может вообще не быть программистом и не иметь доступа к исходным текстам программы и к среде Delphi. Администратор наверняка предпочтет работать с информацией о конфигурации FTP-сервера на вкладке tsOptions (обратите внимание: любые изменения в конфигурации учитываются только при загрузке и запуске приложения, поэтому, чтобы они подействовали, придется перезапустить FTP-сервер). Эта вкладка показана на рис. 7.3.
Рис. 7.2. Свойства CsKeeper1 в инспекторе объектов
Рис. 7.3. Вкладка Options в режиме конструирования
Delphi 3: библиотека программиста
Авторы: Д. Тейлор, Дж. Мишель, Дж. Пенман
(c) Издательство "Питер", 1998
Демонстрационная программа
Мне потребовалась простая программа, которая бы демонстрировала возможности модуля PakTable. На рис. 14.2 показано, как она выглядит при работе. Исходный текст программы приведен в листинге 14.6.
Рис. 14.2. Программа Packing Demo
Листинг 14.6. Демонстрационная программа для упаковки
{————————} { Упаковка таблиц (демонстрационная программа) } { PackMain.PAS : Главная форма } { Автор: Эйс Брейкпойнт, N.T.P. } { При содействии Дона Тейлора } { } { Программа, демонстрирующая применение модуля } { PakTable для упаковки таблиц Paradox и dBASE.} { } { Написано для *High Performance Delphi 3 } Programming* } { Copyright (c) 1997 The Coriolis Group, Inc.} { Дата последней редакции 3/5/97 } {————————} unit PackMain; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, DB, DBTables, StdCtrls, Grids, DBGrids, PakTable, ExtCtrls; type TForm1 = class(TForm) AddBtn: TButton; RemoveBtn: TButton; PackBtn: TButton; QuitBtn: TButton; Table1: TTable; DataSource1: TDataSource; DBGrid1: TDBGrid; Label1: TLabel; TableNameLabel: TLabel; Label2: TLabel; FileSizeLabel: TLabel; Label3: TLabel; NumRecsLabel: TLabel; Bevel1: TBevel; Table1MessageString: TStringField; Table1ID: TAutoIncField; procedure QuitBtnClick(Sender: TObject); procedure FormCreate(Sender: TObject); procedure AddBtnClick(Sender: TObject); procedure RemoveBtnClick(Sender: TObject); procedure PackBtnClick(Sender: TObject); procedure FormActivate(Sender: TObject); private TablePathName : ShortString; procedure UpdateFileLabels; public { Public declarations } end; var Form1: TForm1; implementation {$R *.DFM} procedure TForm1.QuitBtnClick(Sender: TObject); begin Close; end; procedure TForm1.FormCreate(Sender: TObject); var s : ShortString; begin Table1.Active := True; s := Application.ExeName; TablePathName := Copy(s, 1, pos(".", s)) + "DB"; TableNameLabel.Caption := TablePathName; end; procedure TForm1.UpdateFileLabels; var f : File of Byte; begin { При открытой таблице доступ к ее файлу невозможен } Table1.Close; AssignFile(f, TablePathName); {$I-} Reset(f); {$I+} if IOResult = 0 then begin FileSizeLabel.Caption := IntToStr(FileSize(f)); CloseFile(f); end else FileSizeLabel.Caption := "I/O error!"; { Снова открываем таблицу } Table1.Open; NumRecsLabel.Caption := IntToStr(Table1.RecordCount); end; procedure TForm1.AddBtnClick(Sender: TObject); var i : Integer; begin with Table1 do begin for i := 1 to 100 do begin Append; Table1.FieldByName ("MessageString").AsString := IntToStr(i) + ": Hello. My name is Mister Ed."; Post; end; { for } end; { with } UpdateFileLabels; end; procedure TForm1.RemoveBtnClick(Sender: TObject); begin with Table1 do begin First; while not EOF do begin Edit; Delete; MoveBy(3); end; { while } end; { with } UpdateFileLabels; end; procedure TForm1.PackBtnClick(Sender: TObject); begin if not PackTable(Table1) then MessageDlg("Error packing the table", mtError, [mbOK], 0); UpdateFileLabels; end; procedure TForm1.FormActivate(Sender: TObject); begin UpdateFileLabels; end; end.Это простое приложение демонстрирует процесс упаковки файлов Paradox. При нажатии кнопки Add в таблицу добавляются 100 новых записей; кнопка Remove удаляет каждую третью запись. Если несколько раз нажать Add и Remove и при этом следить за отображаемой информацией, становится очевидно, что операция удаления освобождает не все неиспользуемое место. Нажатие кнопки Pack Table не изменяет количества записей, но может заметно сократить общий размер файла.
Конец записи (20 марта).
Динамические данные и статические объявления
Модуль Math быстро работает и обладает широким набором функций, но таит в себе и ловушки. Чтобы получить максимум пользы от статистических функций, необходимо знать пару фокусов. Видите ли, многие функции модуля Math получают параметр, объявленный в виде
const Data: array of Double
Использование таких функций осложняется тем, что массив, передаваемый подобным образом, должен быть объявлен статическ и. С первого взгляда кажется, что передать этим функциям динамические данные невозможно. Большинство программистов находят для дилеммы динамических данных два обходных пути. Они:
«Зашивают» данные в программу (жесткое кодирование). Создают огромный массив и надеются, что пользователь не выйдет за его пределы.Иногда жесткое кодирование неизбежно, но чаще всего о нем даже не стоит думать. В нашем случае дело обстоит именно так. Рассмотрим следующий вызов функции Mean:
Mean([3, 2, 1, 5, 6]);
Фактически эта строка представляет собой калькулятор, который всегда выдает один и тот же результат. Не слишком полезный вариант, не правда ли1?
Понятно, что жесткое кодирование не решает проблем. Остается объявление массива «с запасом». Хотя в некоторых ситуациях такая методика чрезвычайно полезна (а иногда даже необходима), она может приводить к непредвиденным осложнениям.
Это особенно справедливо для модуля Math. Снова рассмотрим функцию Mean. «Среднее арифметическое» определяется как сумма N чисел, деленная на N. Предположим, у нас имеется массив из 10 000 элементов, который мы собираемся передать функции Mean. Если пользователь введет значения только для 50 элементов, знаменатель (N) будет по-прежнему равен 10 000 и превысит правильное значение на 9 950! Как говорится, приплыли…
Динамический пользовательский интерфейс
Теренс Гоггин
Если пользователям не нравится тот интерфейс, который вы им предлагаете, то почему бы не позволить им самостоятельно переделать его во время работы программы? Имитировать режим конструирования во время выполнения оказывается проще, чем вы думаете, причем это может радикально сказаться на привлекательности вашего приложения.
Признаем очевидный факт: люди смотрят на одни и те же вещи по-разному. Если бы мнения пользователей насчет представления данных совпадали, существовала бы всего одна персональная информационная система (Personal Information Manager, PIM). Но этого не происходит— рынок забит PIM'ами всех размеров и мастей.
Некоторым разработчикам удается отыскать удачные интерфейсные решения, и их продукты немедленно обретают всеобщее признание. Другие программы сложны и кажутся интуитивно понятными разве что своим создателям. Похоже, третьего не дано.
Иногда сложная в использовании программа оказывается настолько полезной, что пользователи заставляют себя работать с ней, как бы трудно им ни было. Но не стоит рассчитывать на это при проектировании новой программы, лучше сразу приготовиться к жалобам.
Идеальный пример — панель инструментов MS Word 6.0. Возможно, вам всегда было понятно, зачем нужны эти кнопочки с кривыми стрелочками. С другой стороны, вы могли решить, что панель слишком загромождена и непонятна. Промежуточных вариантов опять же не бывает: интуиция говорит либо «да», либо «нет».
Поскольку любая компания в конечном счете стремится продать как можно больше своих продуктов, разработчики графических интерфейсов не могут просто игнорировать клиентов, живущих под девизом «все не так» — но они не могут и менять весь дизайн проекта в угоду прихотям отдельных пользователей.
До сих пор никто толком не занимался этой проблемой. Никто не пытался разработать для конечного пользователя интерфейс, построенный по принципу «сделай сам». Но достаточно взять Delphi 2 или Delphi 3, добавить немного изобретательности — и перед вами инспектор объектов, встроенный прямо в программу!
Сначала мы посмотрим, как может выглядеть простейшее приложение для работы с базой данных, поддерживающее динамическое конструирование. Затем мы обсудим некоторые механизмы, которые делают подобный интерфейс возможным.
DLL: недостатки и предостережения
Большинство программистов после знакомства с новой концепцией начинают вести себя, как маньяк с новой бензопилой— им не терпится опробовать новинку в деле. Порой они проявляют чудеса извращенной изобретатель ности, чтобы оправдать ее применение в конкретной ситуации. Как бы трудно вам ни было, постарайтесь удержаться. Несомненно, DLL — классная штука, но она может легко превратиться в источник сплошных бед.
Даже не пытайтесь вынести в DLL какие-либо обязательные возможности вашей программы. Например, подсистема форматирования текста в редакторе должна относиться к программе, а не к внешней DLL. DLL следует приберечь для необязательных возможностей (в том числе и дополнений, написанных посторонними фирмами) и общих библиотек. Вс?! Применяя DLL для других целей, вы сами напрашиваетесь на неприятности.
Самый большой недостаток DLL — проверка типов (а вернее, ее отсутствие). Обращаясь к функции DLL при любом способе импорта, вы фактически приказываете компилятору вызвать функцию, о которой он ничего не знает. Например, в модуле BEEPDLL.PAS содержится следующее объявление:
procedure BeepMe; external "beeper.dll";
Данное объявление просто сообщает компилятору о том, что существует некая процедура BeepMe и она находится в указанной DLL. Замечательно. Компилятор верит вам на слово. Он никак не может найти файл BEEPER.DLL, дизассемблировать его и убедиться, что в нем действительно есть процедура с именем BeepMe и что она вызывается без параметров. Если процедура BeepMe в DLL должна получать один или несколько параметров (или в случае процедуры с параметрами — параметры другого типа), при вызове BeepMe разверзнется сущий ад: процедура получит неверное количество параметров или они будут иметь неверный тип. Гарантирую, что это когда-нибудь случится и с вами. По своему опыту знаю, что найти подобную ошибку очень сложно. Стыдно признаваться, но я и сам столкнулся с этой проблемой вскоре после того, как написал предыдущую фразу, во время работы над программой для следующего раздела.
Если вас интересует более подробное (и устрашающее) описание проблем, связанных с DLL, почитайте книгу Лу Гринзо (Lou Grinzo) «Zen of Windows 95 Programming» (Coriolis Group Books, 1995). Эта превосходная книга содержит массу полезной информации о программировании для Windows, а также ряд хороших советов по поводу программирования вообще. Для программирования необходима паранойя (в разумных дозах) и твердая вера в справедливость законов Мерфи. Даже если вы не верите в это сейчас, то после прочтения книги Лу непременно поверите.
Я заканчиваю выступление и слезаю с трибуны, и не говорите потом, что вас не предупреждали. Теперь вы знаете, как создавать DLL, так давайте посмотрим, что можно сделать с их помощью.
Другие применения
EMBEDDEFORMS.DPR демонстрирует лишь два первых сценария из четырех, описанных в начале этой главы, — использование одной и той же формы для мастера и списка свойств, а также использование форм как компонентов. Я не привел ни одного реального примера для двух последних сценариев, связанных с использованием внедренных форм для совместной разработки диалогового окна со вкладками или с построением универсального редактора, способного работать с любым объектом иерархии. Тем не менее я продемонстрировал всю методику, необходимую для реализации этих, более редких сценариев.
Чтобы построить диалоговое окно из нескольких независимых форм, достаточно породить каждую из них от TEmbeddedForm. Создайте вкладку для каждой страницы и в обработчике OnCreate диалогового окна вызовите Create Embedded для формы каждой страницы. Обычно я стараюсь соблюдать общее правило «Сам создал — сам уничтожай» и аккуратно уничтожаю страницы в обработчике OnDestroy, но, строго говоря, без этого можно обойтись, так как при уничтожении диалогового окна уничтожаются все его дочерние компоненты. Если вам потребуется постраничная проверка корректности (например, чтобы пользователь не мог покинуть страницу с неверными данными), используйте TFickleView вместо TEmbeddedForm.
Универсальный редактор можно построить на основе абстрактного редактора моделей — объект-контейнер содержит все стандартные элементы, а пустая панель-фрейм предназначена для размещения специализированных элементов. Для каждого члена иерархии объектов можно создать функцию класса (class function), которая возвращает TViewClass. Это позволит универсальному редактору заполнить фрейм правильным видом, соответствующим редактируемому объекту.
За последний год мне пришлось довольно много возиться с внедренными формами. Они помогают существенно упростить программу, сделать ее более надежной и гибкой. Такая возможность всегда присутствовала в Windows, но она оставалась невероятно сложной до тех пор, пока среда Delphi не сделала ее простой.
Другой подход к потокам
Возможно, вы заметили, что класс TFileStream тоже содержит методы для сохранения и загрузки свойств компонента. Хотя TFileStream содержит целых два набора методов для сохранения и загрузки компонентов, эти методы выполняют лишнюю работу, что снижает эффективность такого варианта по сравнению с выбранной нами реализацией TReader/TWriter.
Методы WriteComponentRes и ReadComponentRes сохраняют и загружают компоненты в формате стандартных ресурсов Windows. Это связано с лишней вычислительной нагрузкой. К тому же многие данные, сохраняемые этими методами, просто не представляют для нас интереса и лишь увеличивают размер файла свойств.
Методы WriteComponents и ReadComponents приводят к тому же конечному результату, что и в нашем случае, но при этом вызывается пара лишних функций. Наш способ работает эффективнее и немного быстрее.
Файловые операции чтения/записи
Разобравшись с анализом командных строк, мы приступаем к следующей крупной подзадаче- файловому вводу/выводу. Разумеется, при простейших посимвольных (или построчных) преобразованиях текстовых файлов можно пользоваться функциями Read и Write (или ReadLn и WriteLn) в сочетании с Eof и Eoln. Например, процедура DoFilter из листинга 1.7 копирует символы из входного файла в выходной, преобразуя их к верхнему регистру.
Листинг 1.7. Перевод символов в верхний регистр
procedure DoFilter; const nOptions = 2; Options : Array [1..nOptions] of OptionRec = ( (OptionChar : "i"; Option : otFilename; Filename : ""), (OptionChar : "o"; Option : otFilename; Filename : "") ); var cRslt : Boolean; iRec : pOptionRec; oRec : pOptionRec; InputFile : Text; OutputFile : Text; c : char; begin cRslt := CmdLine.ProcessCommandLine (@Options, nOptions); if (not cRslt) then Halt; { Убедимся в том, что были заданы имена входного и выходного файлов } iRec := CmdLine.GetOptionRec (@Options, nOptions, "i"); if (iRec^.Filename = "") then begin WriteLn ("Error: input file expected"); Halt; end; oRec := CmdLine.GetOptionRec (@Options, nOptions, "o"); if (oRec^.Filename = "") then begin WriteLn ("Error: output file expected"); Halt; end; { Открываем входной файл - без проверки ошибок} Assign (InputFile, iRec^.Filename); Reset (InputFile); { Создаем выходной файл - без проверки ошибок} Assign (OutputFile, oRec^.Filename); Rewrite (OutputFile); { Читаем и преобразуем каждый символ } while (not Eof (InputFile)) do begin Read (InputFile, c); c := UpCase (c); Write (OutputFile, c); end; Close (InputFile); Close (OutputFile); end;У данной версии программы FILTER есть два недостатка. Во-первых, она еле ползает - словно змея, пробуждающаяся от зимней спячки. Если у вас найдется мегабайтовый текстовый файл и несколько свободных минут, убедитесь сами. Во-вторых, она работает только с текстовыми файлами. Для одноразового приложения сойдет и так, но мы пишем шаблон для различных
программ, которым может понадобиться работать и с двоичными файлами. Да и скорость работы не мешало бы повысить. Поэтому необходимо найти более универсальный и быстрый способ чтения символов (или байтов) из файла. Нам придется самостоятельно организовать буферизацию; программа при этом усложняется, но результат стоит затраченных усилий.
Класс TFilterFile из листинга 1.8 предназначен для организации быстрых побайтовых операций с файлами в программах-фильтрах. Он инкапсулирует все детали буферизации и по возможности избавляет программиста от необходимости помнить о многочисленных житейских проблемах работы с файлами (вам остается лишь вызвать Open и Close).
Листинг 1.8. Реализация класса TFilterFile из файла FILEIO.PAS
{ FILEIO.PAS - Файловый ввод/вывод для программ-фильтров Автор: Джим Мишель Дата последней редакции: 04/05/97 } {$I+} { Использовать исключения для обработки ошибок } unit fileio; interface type FileIOMode = (fioNotOpen, fioRead, fioWrite); BuffArray = array[0..1] of byte; pBuffArray = ^BuffArray; TFilterFile = class (TObject) private FFilename : String; F : File; FBufferSize : Integer; FBuffer : pBuffArray; FBytesInBuff : Integer; FBuffIndx : Integer; FFileMode : FileIOMode; function ReadBuffer : boolean; function WriteBuffer : boolean; public constructor Create (AName : String; ABufSize : Integer); destructor Destroy; override; function Open (AMode : FileIOMode) : Boolean; procedure Close; function Eof : Boolean; function GetByte : byte; function PutByte (b : byte) : boolean; end; implementation { TFilterFile } { Create - подготавливает, но не открывает файл } constructor TFilterFile.Create ( AName : String; ABufSize : Integer ); begin inherited Create; FFilename := AName; FBufferSize := ABufSize; FBytesInBuff := 0; FBuffIndx := 0; FFileMode := fioNotOpen; { Назначаем, но не открываем } Assign (F, FFilename); { Выделяем память для буфера } GetMem (FBuffer, FBufferSize); end; { Destroy - закрывает файл (если он открыт) и уничтожает объект } destructor TFilterFile.Destroy; begin { Если файл открыт, закрываем его } if (FFileMode <> fioNotOpen) then begin Self.Close; end; { Если был выделен буфер, освобождаем его } if (FBuffer <> Nil) then begin FreeMem (FBuffer, FBufferSize); FBuffer := Nil; end; inherited Destroy; end; { Open - открыть файл в нужном режиме } function TFilterFile.Open ( AMode : FileIOMode ) : Boolean; var SaveFileMode : Byte; begin Result := True; SaveFileMode := FileMode; { переменная FileMode определена в модуле System } { Пытаемся открыть файл } try case AMode of fioRead : begin FileMode := 0; Reset (F, 1); end; fioWrite : begin FileMode := 1; Rewrite (F, 1); end; end; FFileMode := AMode; except Result := False; end; FBytesInBuff := 0; FBuffIndx := 0; FileMode := SaveFileMode; end; { Close - закрывает файл, при необходимости сбрасывая буфер } procedure TFilterFile.Close; begin { Если буфер записи не пуст, записываем его } if ((FFileMode = fioWrite) and (FBytesInBuff > 0)) then begin WriteBuffer; end; try { Закрываем файл } System.Close (F); finally FFileMode := fioNotOpen; end; end; { ReadBuffer - читает блок из файла в буфер } function TFilterFile.ReadBuffer : Boolean; begin Result := True; if (Self.Eof) then begin Result := False; end else begin try BlockRead (F, FBuffer^, FBufferSize, FBytesInBuff); except Result := False; end; end; end; { GetByte - возвращает следующий байт из файла. При необходимости читает из файла в буфер } function TFilterFile.GetByte : byte; begin if (FBuffIndx >= FBytesInBuff) then begin if (not ReadBuffer) then begin Result := 0; Exit; end else begin FBuffIndx := 0; end; end; Result := FBuffer^[FBuffIndx]; Inc (FBuffIndx); end; { WriteBuffer - записывает блок из буфера в файл } function TFilterFile.WriteBuffer : Boolean; begin Result := True; try BlockWrite (F, FBuffer^, FBytesInBuff); except Result := False; end; if (Result = True) then begin FBytesInBuff := 0; end; end; { PutByte - заносит байт в буфер. При необходимости записывает буфер в файл } function TFilterFile.PutByte (b : byte) : Boolean; begin if (FBytesInBuff = FBufferSize) then begin if (not WriteBuffer) then begin Result := False; Exit; end else begin FBytesInBuff := 0; end; end; FBuffer^[FBytesInBuff] := b; Inc (FBytesInBuff); Result := True; end; { Eof - возвращает True, если был достигнут конец входного файла } function TFilterFile.Eof : Boolean; begin Result := (FBuffIndx >= FBytesInBuff); if Result then begin try Result := System.Eof (F); except Result := True; end; end; end; end.Поскольку класс TFilterFile почти все делает сам, использовать его вместо стандартного текстового файла ввода/вывода оказывается очень просто. Тем не менее скорость работы меняется прямо на глазах. Новая процедура DoFilter из листинга 1.9 использует класс TFilterFile для выполнения файловых операций. Получившаяся программа работает намного быстрее первоначальной версии. А самое приятное заключается в том, что прочесть или понять ее оказывается ничуть не сложнее, чем предыдущий, медленный вариант.
Листинг 1.9. Использование класса TFilterFile вместо
стандартного файлового ввода/вывода
Фильтры
Вероятно, из всех средств командной строки на персональных компьютерах чаще всего встречаются программы, принадлежащие к широкой категории «фильтров». Фильтром может быть все, что угодно, -от простейшего счетчика строк до сложного компилятора (например, компилятора языка Паскаль из Delphi), утилиты сортировки или программы пакетных вычислений.
Все фильтры построены на одном принципе: они вызываются из командной строки и получают аргументы, в которых задаются параметры их работы, а также имена входных и выходных файлов. Фильтр читает входные данные, выполняет некоторые вычисления (зависящие от параметров, указанных в командной строке) и записывает результат в выходной файл.
Фильтры обычно не работают с мышью и вообще очень редко взаимодействуют с пользователем. Если же фильтр все-таки получает информацию от пользователя, то для этого применяется простейший текстовый интерфейс. Вывод, как правило, ограничивается информацией о ходе процесса («Working, please wait…»), сообщениями об ошибках и завершающим сообщением «Done».
В этой главе мы напишем на Delphi относительно простую программу -фильтр, построив при этом «каркас», на основе которого можно будет легко создавать другие фильтры. Попутно мы узнаем кое-что о хранилище объектов Delphi, многократном использовании кода и (содрогнитесь от ужаса) процессно-ориентированном программировании.
Замечание
Ирония судьбы - всего три года назад я преподавал программирование для Windows DOS-программистам и рассказывал им о том, как отказаться от традиционного процессно-ориен тированного мышления и войти в широкий мир управляемых событиями Windows-программ. С появлением визуальных средств разработки - таких как Visual Basic и Delphi - многие новички сразу начинают с событийного программирования и даже не умеют писать процессно-ориентированные средства командной строки. А теперь я рассказываю вам о том, как от событийного программирования вернуться к процессно-ориентированному. Plus зa change.
Единственный «плюс» заключается в том, что программист, привыкший работать с событиями, без особых трудностей поймет процессно-ориентированный код. Обратное, к сожалению, неверно.
Финансовые функции и процедуры
DoubleDecliningBalance Вычисление амортизации методом двойного баланса
FutureValue Будущее значение вложения
InterestPayment Вычисление процентов по ссуде
InterestRate Норма прибыли, необходимая для получения заданной
суммы
InternalRateOfReturn Вычисление внутренней скорости оборота вложения для
ряда последовательных выплат
NetPresentValue Вычисление чистой текущей стоимости вложения для
ряда последовательных выплат с учетом процентной
ставки
NumberOfPeriods Количество периодов, за которое вложение достигнет
заданной величины
Payment Размер периодической выплаты, необходимой для пога-
шения ссуды, при заданном числе периодов, процентной
ставке, а также текущем и будущем значениях ссуды
PeriodPayment Платежи по процентам за заданный период
PresentValue Текущее значение вложения
SLNDepreciation Вычисление амортизации методом постоянной нормы
SYDepreciation Вычисление амортизации методом весовых коэф-
фициентов
Где и как хранится конфигурация
Все параметры конфигурации, не считая текстовых файлов с приветственным и прощальным сообщениями, хранятся в системном реестре Windows 95 или NT4.0. Для загрузки и сохранения этих сообщений используется класс Delphi TRegistry. При запуске приложения KEEPER32 обработчик frmMain.OnCreate вызывает процедуру LoadSettings для чтения параметров из реестра Windows. Листинг 7.2 показывает, как это делается. После чтения из реестра LoadSettings обновляет свойства CsKeeper1 в соответствии с полученными значениями.
Листинг7.2. Процедура LoadSettings
procedure TfrmMain.LoadSettings; var Reg : TRegistry; Count : Integer; IPName : String; begin Reg := TRegistry.Create; // Чтение параметров try Reg.OpenKey(FtpServerKey, TRUE); if Reg.ValueExists('DRootDisk') then CsKeeper1.RootDisk := Reg.ReadString('DRootDisk') else CsKeeper1.RootDisk := ''; if Reg.ValueExists('DRootDir') then CsKeeper1.RootDir := Reg.ReadString('DRootDir') else CsKeeper1.RootDir := ''; finally Reg.CloseKey; end; try Reg.OpenKey(FtpServerKey, TRUE); if Reg.ValueExists('DTransferMode') then begin OldTransferMode := Reg.ReadString('DTransferMode'); if UpperCase(OldTransferMode) = UpperCase(FtpTransferStr[STREAM]) then begin CsKeeper1.Transfer := STREAM; rgTransfer.ItemIndex := 0; end; if UpperCase(OldTransferMode) = UpperCase(FtpTransferStr[BLOCK]) then begin CsKeeper1.Transfer := BLOCK; rgTransfer.ItemIndex := 1; end; if UpperCase(OldTransferMode) = UpperCase(FtpTransferStr[COMPRESSED]) then begin CsKeeper1.Transfer := COMPRESSED; rgTransfer.ItemIndex := 2; end; end else begin OldTransferMode := UpperCase(FtpTransferStr[STREAM]); CsKeeper1.Transfer := STREAM; end; finally Reg.CloseKey; end; // Свойство файловой структуры try Reg.OpenKey(FtpServerKey, TRUE); if Reg.ValueExists('DFileStructure') then begin OldFileStruct := Reg.ReadString('DFileStructure'); if UpperCase(OldFileStruct) = UpperCase(FtpFileStructStr[NOREC]) then begin CsKeeper1.FileStruct := NOREC; rgFileStructure.ItemIndex := 0; end; if UpperCase(OldFileStruct) = UpperCase(FtpFileStructStr[REC]) then begin CsKeeper1.FileStruct := REC; rgFileStructure.ItemIndex := 1; end; if UpperCase(OldFileStruct) = UpperCase(FtpFileStructStr[PAGE]) then begin CsKeeper1.FileStruct := PAGE; rgFileStructure.ItemIndex := 2; end; end else begin OldFileStruct := UpperCase(FtpFileStructStr[NOREC]); CsKeeper1.FileStruct := NOREC; rgFileStructure.ItemIndex := 0; end; finally Reg.CloseKey; end; // Разрешение на создание новых каталогов try Reg.OpenKey(FtpServerKey, TRUE); if Reg.ValueExists('DCreateNewDir') then begin OldMkDir := Reg.ReadBool('DCreateNewDir'); CsKeeper1.CreateDir := OldMkDir; if OldMkDir then cbAllowMkDir.State := cbChecked else cbAllowMkDir.State := cbUnChecked; end else begin OldMkDir := FALSE; CsKeeper1.CreateDir := OldMkDir; end; finally Reg.CloseKey; end; // Разрешение на удаление каталогов try Reg.OpenKey(FtpServerKey, TRUE); if Reg.ValueExists('DDeleteDir') then begin OldDeleteDir := Reg.ReadBool('DDeleteDir'); CsKeeper1.DeleteDir := OldDeleteDir; if OldDeleteDir then cbDeleteDir.State := cbChecked else cbDeleteDir.State := cbUnChecked; end else begin OldDeleteDir := FALSE; CsKeeper1.DeleteDir := OldDeleteDir; cbDeleteDir.State := cbUnChecked; end; finally Reg.CloseKey; end; // Разрешение на передачу файлов try Reg.OpenKey(FtpServerKey, TRUE); if Reg.ValueExists('DUpLoads') then begin OldUpLoads := Reg.ReadBool('DUpLoads'); CsKeeper1.UpLoads := OldUpLoads; if OldUpLoads then cbUpLoad.State := cbChecked else cbUpLoad.State := cbUnChecked; end else begin OldUpLoads := FALSE; CsKeeper1.UpLoads := OldUpLoads; cbUpLoad.State := cbUnChecked; end; finally Reg.CloseKey; end; try Reg.OpenKey(FtpServerKey, TRUE); if Reg.ValueExists('DNoBannedIPs') then NoOfBannedIPs := Reg.ReadInteger ('DNoBannedIPs') else NoOfBannedIPs := 1; finally Reg.CloseKey; end; // Список запрещенных IP-адресов for Count := 0 to NoOfBannedIPs - 1 do begin IPName := Concat('IPName', IntToStr(Count)); try Reg.OpenKey(FtpServerKey + '\IPs' + '\ ' + IPName, TRUE); if Reg.ValueExists('IPName') then lbBadIPAddrs.Items.Add(Reg.ReadString ('IPName')) else lbBadIPAddrs.Items.Add(''); OldBannedIPsList.Add(lbBadIPAddrs.Items.Strings [Count]); finally Reg.CloseKey; end; end; // цикл for with CsKeeper1 do begin if Length(RootDisk) > 0 then dcbRootDisk.Drive := Char(RootDisk[1]) else dcbRootDisk.Drive := 'C'; if Length(RootDir) > 0 then dlbRootDir.Directory := RootDir; for Count := 0 to NoOfBannedIPs - 1 do BadIPs.Add(lbBadIPAddrs.Items.Strings[Count]); end; Reg.Free; end;Где Windows ищет DLL
Если в вашем приложении используется DLL, установочная программа обычно помещает ее в один каталог с исполняемым файлом программы. В этом случае у Windows не возникнет никаких проблем с поиском DLL при загрузке программы (или при вызове LoadLibrary, если вы выбрали динамический импорт). Если приложение помещает несколько исполняемых файлов в различные каталоги, вы можете либо скопировать DLL в каждый из этих каталогов (что отчасти противоречит главной цели DLL), либо поместить DLL в один общий каталог, просматриваемый Windows по умолчанию при загрузке DLL.
Итак, Windows ищет DLL в следующих местах (и в следующем порядке):
Каталог, из которого было загружено приложение. Текущий каталог. Системный каталог Windows. Только для Windows NT: системный каталог 16-разрядной Windows. Каталог Windows.Каталоги, перечисленные в переменной окружения PATH.
В случае динамического импорта при вызове LoadLibrary можно указать для DLL полный путь, тогда Windows просмотрит только заданный каталог. Если вы хотите, чтобы Windows автоматически загружала DLL при запуске (статический импорт), такой возможности уже не будет.
Генерация и отображение ландшафта
После такого внушительного пролога код для генерации ландшафта выглядит на удивление просто. Процедура FractureTriangle() (см.листинг 8.2) получает треугольник и количество остающихся итераций Plys. Если Plys превышает 1, FractureTriangle() вызывает FractureLine() для расчета (или получения готовых) высот середин отрезков, а затем вызывает себя для каждого из четырех треугольников, которые получаются после разделения. FractureLine() вызывает Midpoint() (обе процедуры приведены в листинге 8.2), чтобы вычислить среднюю точку отрезка, образованного двумя вершинами, и затем смотрит, была ли ее высота задана ранее. Если середина еще не инициализирована, FractureLine() изгибает отрезок, поднимая или опуская его середину.
После того как ландшафт будет рассчитан, FL3 отображает его в текущем окне и в текущем режиме отображения с помощью кода, приведенного в листинге 8.3. При изменении размеров окна или режима отображения FL3 перерисовывает ландшафт.
Листинг 8.3. Модуль DISPLAY.PAS
unit Display; { Fractal Landscapes 3.0 - Copyright © 1987..1997, Джон Шемитц } interface uses WinTypes, WinProcs, SysUtils, Graphics, Forms, Global, Database; const DrawingNow: boolean = False; AbortDraw: boolean = False; type EAbortedDrawing = class (Exception) end; procedure ScreenColors; procedure PrinterColors; procedure DrawTriangle( Canvas: TCanvas; const A, B, C: TVertex; Plys: word; PointDn: boolean); procedure DrawVerticals(Canvas: TCanvas); {$ifdef Debug} const DebugString: string = ''; {$endif} implementation uses Main; type Surfaces = record Outline, Fill: TColor; end; const scrnLand: Surfaces = (Outline: clLime; Fill: clGreen); scrnWater: Surfaces = (Outline: clBlue; Fill: clNavy); scrnVertical: Surfaces = (Outline: clGray; Fill: clSilver); prnLand: Surfaces = (Outline: clBlack; Fill: clWhite); prnWater: Surfaces = (Outline: clBlack; Fill: clWhite); prnVertical: Surfaces = (Outline: clBlack; Fill: clWhite); var Land, Water, Vertical: Surfaces; procedure ScreenColors; begin Land := scrnLand; Water := scrnWater; Vertical := scrnVertical; end; procedure PrinterColors; begin Land := prnLand; Water := prnWater; Vertical := prnVertical; end; function Surface(Outline, Fill: TColor): Surfaces; begin Result.Outline := Outline; Result.Fill := Fill; end; { $define Pascal} {$define Float} {$ifdef Pascal} {$ifdef Float} type TFloatTriple = record X, Y, Z: double; end; function FloatTriple(T: TTriple): TFloatTriple; begin Result.X := T.X / UnitLength; Result.Y := T.Y / UnitLength; Result.Z := T.Z / UnitLength; end; function Project(const P: TTriple): TPixel; { Перспективное преобразование координат точки } var Delta_Y: double; Tr, V: TFloatTriple; begin Tr := FloatTriple(P); V := FloatTriple(VanishingPoint); Delta_Y := Tr.Y / V.Y; Result.X := Round( DisplayWidth * ((V.X - Tr.X) * Delta_Y + Tr.X)); Result.Y := DisplayHeight - Round( DisplayHeight * ((V.Z - Tr.Z) * Delta_Y + Tr.Z)); end; {$else} function Project(const Tr: TTriple): TPixel; { Перспективное преобразование координат точки } var Delta_Y: integer; begin Delta_Y := MulDiv(Tr.Y, UnitLength, VanishingPoint.Y); Result.X := MulDiv( MulDiv ( VanishingPoint.X - Tr.X, Delta_Y, UnitLength) + Tr.X, DisplayWidth, UnitLength); Result.Y := DisplayHeight - MulDiv( MulDiv( VanishingPoint.Z - Tr.Z, Delta_Y, UnitLength) + Tr.Z, DisplayHeight, UnitLength ); end; {$endif} {$else} function Project(const Tr: TTriple): TPixel; assembler; { Перспективное преобразование координат точки } asm {$ifdef Ver80} {Delphi 1.0; 16-bit} les di,[Tr] mov si,word ptr UnitLength { Масштабный коэффициент } mov ax,[TTriple ptr es:di].Y{ Tr.Y } imul si { Умножаем на LoWord(UnitLength) } idiv VanishingPoint.Y { Scaled(depth/vanishing.depth) } {DeltaY equ bx } mov bx,ax { Сохраняем Delta.Y } mov ax,VanishingPoint.Z sub ax,[TTriple ptr es:di].Z{ Delta.Z } imul bx { Delta.Z * Delta.Y } idiv si { Unscale(Delta.Z * Delta.Y) } add ax,[TTriple ptr es:di].Z { Tr.Z + Unscale(Delta.Z * Delta.Y) } mov cx,[DisplayHeight] { Используем дважды... } imul cx { (Tr.Z+Delta.Z*Delta.Y)*Screen.Row } idiv si { Unscale } sub cx,ax { Px.Y } mov ax,VanishingPoint.X sub ax,[TTriple ptr es:di].X { Delta.X } imul bx { Delta.X * Delta.Y } idiv si { Unscale(Delta.X * Delta.Y) } add ax,[TTriple ptr es:di].X { Tr.X + Unscale(Delta.X * Delta.Y) } imul [DisplayWidth] { (Tr.X+Delta.X*Delta.Y)*Screen.Col} idiv si { Px.X := Unscale(см. выше) } mov dx,cx {Возвращаем (X,Y) в ax:dx} {$else} {Delphi 2.0 or better; 32-bit} push ebx { Delphi 2.0 требует, чтобы } push esi { значения этих регистров } push edi { были сохранены } mov edi,eax { lea edi,[Tr]} push edx { Сохраняем @Result } mov si,word ptr UnitLength { Масштабный коэффициент } mov ax,TTriple[edi].Y { Tr.Y } imul si { Умножаем на } { LoWord(UnitLength) } idiv VanishingPoint.Y { отношение глубины текущей точки к глубине точки перспективы} {DeltaY equ bx } mov bx,ax { Сохраняем Delta.Y } mov ax,VanishingPoint.Z sub ax,TTriple[edi].Z { Delta.Z } imul bx { Delta.Z * Delta.Y } idiv si { Unscale(Delta.Z * Delta.Y) } add ax,TTriple[edi].Z { Tr.Z + Unscale(Delta.Z * Delta.Y) } mov cx,[DisplayHeight] { Используем дважды... } imul cx { (Tr.Z+Delta.Z*Delta.Y)*Screen.Row } idiv si { Unscale } sub cx,ax { Px.Y } mov ax,VanishingPoint.X sub ax,TTriple[edi].X { Delta.X } imul bx { Delta.X * Delta.Y } idiv si { Unscale(Delta.X * Delta.Y) } add ax,TTriple[edi].X { Tr.X + Unscale(Delta.X * Delta.Y) } imul [DisplayWidth] { (Tr.X+Delta.X*Delta.Y)*Screen.Col } idiv si { Px.X := Unscale(см. выше) } // Теперь ax=x, cx=y; мы хотим превратить //их в longint // и сохранить в Result mov ebx,$0000FFFF and eax,ebx { Очищаем старшее слово} and ecx,ebx pop edx { Восстанавливаем результат } mov TPixel[edx].X,eax mov TPixel[edx].Y,ecx pop edi pop esi pop ebx {$endif} end; {$endif} procedure DrawPixels(const Canvas: TCanvas; const A, B, C, D: TPixel; const N: word; const Surface: Surfaces); begin if AbortDraw then raise EAbortedDrawing.Create(''); Canvas.Pen.Color := Surface.Outline; if DrawMode = dmOutline then if N = 3 then Canvas.PolyLine( [A, B, C, A] ) else Canvas.PolyLine( [A, B, C, D, A] ) else begin Canvas.Brush.Color := Surface.Fill; if N = 3 then Canvas.Polygon( [A, B, C] ) else Canvas.Polygon( [A, B, C, D] ) end; end; procedure CalcCrossing(var Low, High, Crossing: TTriple; SetLow: boolean); var CrossOverRatio: LongInt; begin CrossOverRatio := (SeaLevel - Low.Z) * UnitLength div (High.Z - Low.Z); { Расстояние от точки пересечения до A рассчитывается как отношение } { длины отрезка к полной длине AB, умноженное на UnitLength } Crossing := Triple( Low.X + Unscale ((High.X - Low.X) * CrossOverRatio), Low.Y + Unscale((High.Y - Low.Y) * CrossOverRatio), SeaLevel ); if SetLow then Low.Z := SeaLevel; end; procedure DrawVertical(Canvas: TCanvas; const A, B: TTriple; var pA, pB: TPixel); var pC, pD: TPixel; tC, tD: TTriple; begin tC := A; tC.Z := SeaLevel; pC := Project(tC); tD := B; tD.Z := SeaLevel; pD := Project(tD); DrawPixels(Canvas, pA, pB, pD, pC, 4, Vertical); end; procedure DrawVerticals(Canvas: TCanvas); type Triad = record T: TTriple; V: TVertex; P: TPixel; end; var Work: Triad; procedure Step( const Start: TVertex; var Front: Triad; var StepDn: GridCoordinate ); var Idx: word; Back, Interpolate: Triad; begin Back.V := Start; Back.T := GetTriple(Back.V); if Back.T.Z > SeaLevel then Back.P := Project(Back.T); for Idx := 1 to EdgeLength do begin Front.V := Back.V; Inc(Work.V.BC); Dec(StepDn); Front.T := GetTriple(Front.V); if Front.T.Z > SeaLevel then Front.P := Project(Front.T); case (ord(Back.T.Z > SeaLevel) shl 1) + ord(Front.T.Z > SeaLevel) of 1: begin { Задняя точка ниже уровня моря, передняя - выше } CalcCrossing(Back.T, Front.T, Interpolate.T, False); Interpolate.P := Project (Interpolate.T); DrawVertical(Canvas, Interpolate.T, Front.T, Interpolate.P, Front.P); end; 2: begin { Задняя точка выше уровня моря, передняя - ниже } CalcCrossing(Front.T, Back.T, Interpolate.T, False); Interpolate.P := Project(Interpolate.T); DrawVertical(Canvas, Back.T, Interpolate.T, Back.P, Interpolate.P); end; 3: DrawVertical(Canvas, Back.T, Front.T, Back.P, Front.P); { Обе точки выше уровня моря } end; Back := Front; end; end; begin Step(C, Work, Work.V.AB ); Step(B, Work, Work.V.CA ); end; function InnerProduct({const} A, B: TTriple): LongInt; begin InnerProduct := IMUL(A.X, B.X) + IMUL(A.Y, B.Y) + IMUL(A.Z, B.Z) ; end; function Delta(A, B: TTriple): TTriple; begin Result := Triple(A.X - B.X, A.Y - B.Y, A.Z - B.Z); end; function LandColor(const A, B, C: TTriple): TColor; var Center, ToA, ToLight: TTriple; Cos, Angle: double; GrayLevel: integer; begin Center := Triple( (A.X + B.X + C.X) div 3, (A.Y + B.Y + C.Y) div 3, (A.Z + B.Z + C.Z) div 3 ); ToA := Delta(A, Center); ToLight := Delta(Center, LightSource); {$ifopt R-} {$define ResetR} {$endif} {$R+} try Cos := InnerProduct(ToA, ToLight) / (Sqrt({Abs(}InnerProduct(ToA, ToA){)}) * Sqrt({Abs(}InnerProduct(ToLight, ToLight){)}) ); try Angle := ArcTan (Sqrt (1 - Sqr (Cos)) / Cos); except on Exception do Angle := Pi / 2; {ArcCos(0)} end; {$ifdef HighContrast} GrayLevel := 255 - Round(255 * (Abs(Angle) / (Pi / 2))); {$else} GrayLevel := 235 - Round(180 * (Abs(Angle) / (Pi / 2))); {$endif} except on Exception {любое исключение} do GrayLevel := 255; { Деление на 0... } end; {$ifdef ResetR} {$R-} {$undef ResetR} {$endif} Result := PaletteRGB(GrayLevel, GrayLevel, GrayLevel); end; procedure Draw3Vertices( Canvas: TCanvas; const A, B, C: TVertex; Display: boolean); var Color: TColor; pA, pB, pC, pD, pE: TPixel; tA, tB, tC, tD, tE: TTriple; aBelow, bBelow, cBelow: boolean; begin tA := GetTriple(A); tB := GetTriple(B); tC := GetTriple(C); {$ifdef FloatingTriangles} ta.z := ta.z + random(Envelope shr Plys) - random(Envelope shr Plys); tb.z := tb.z + random(Envelope shr Plys) - random(Envelope shr Plys); tc.z := tc.z + random(Envelope shr Plys) - random(Envelope shr Plys); {$endif} aBelow := tA.Z <= SeaLevel; bBelow := tB.Z <= SeaLevel; cBelow := tC.Z <= SeaLevel; case ord(aBelow) + ord(bBelow) + ord(cBelow) of 0: if Display then { Все вершины выше уровня моря } begin pA := Project(tA); pB := Project(tB); pC := Project(tC); if DrawMode = dmRender then begin Color := LandColor(tA, tB, tC); DrawPixels( Canvas, pA, pB, pC, pC, 3, Surface(Color, Color)); end else DrawPixels( Canvas, pA, pB, pC, pC, 3, Land); end; 3: if Display then { Все вершины ниже уровня моря } begin tA.Z := SeaLevel; tB.Z := SeaLevel; tC.Z := SeaLevel; pA := Project(tA); pB := Project(tB); pC := Project(tC); DrawPixels( Canvas, pA, pB, pC, pC, 3, Water); end; 2: begin { Одна вершина над водой } { Сделаем так, чтобы это была вершина tA } if aBelow then if bBelow then SwapTriples(tA, tC) else SwapTriples(tA, tB); CalcCrossing(tB, tA, tD, True); CalcCrossing(tC, tA, tE, True); pA := Project(tA); pB := Project(tB); pC := Project(tC); pD := Project(tD); pE := Project(tE); DrawPixels( Canvas, pD, pB, pC, pE, 4, Water); if Drawmode = dmRender then begin Color := LandColor(tD, tA, tE); DrawPixels( Canvas, pD, pA, pE, pE, 3, Surface(Color, Color)); end else DrawPixels( Canvas, pD, pA, pE, pE, 3, Land); end; 1:begin { Одна вершина под водой } { Сделаем так, чтобы это была вершина tA } if bBelow then SwapTriples(tA, tB) else if cBelow then SwapTriples(tA, tC); CalcCrossing(tA, tB, tD, False); CalcCrossing(tA, tC, tE, True); pA := Project(tA); pB := Project(tB); pC := Project(tC); pD := Project(tD); pE := Project(tE); DrawPixels( Canvas, pD, pA, pE, pE, 3, Water); if DrawMode = dmRender then begin Color := LandColor(tD, tB, tC); DrawPixels( Canvas, pD, pB, pC, pE, 4, Surface(Color, Color)); end else DrawPixels( Canvas, pD, pB, pC, pE, 4, Land); end; end; end; procedure DrawTriangle( Canvas: TCanvas; const A, B, C: TVertex; Plys: word; PointDn: boolean); var AB, BC, CA: TVertex; begin if Plys = 1 then Draw3Vertices(Canvas, A, B, C, (DrawMode <> dmOutline) OR PointDn) else begin AB := Midpoint(A, B); BC := Midpoint(B, C); CA := Midpoint(C, A); if Plys = 3 then FractalLandscape.DrewSomeTriangles(16); Dec(Plys); if PointDn then begin DrawTriangle(Canvas, CA, BC, C, Plys, True); DrawTriangle(Canvas, AB, B, BC, Plys, True); DrawTriangle(Canvas, BC, CA, AB, Plys, False); DrawTriangle(Canvas, A, AB, CA, Plys, True); end else begin DrawTriangle(Canvas, A, CA, AB, Plys, False); DrawTriangle(Canvas, BC, CA, AB, Plys, True); DrawTriangle(Canvas, CA, C, BC, Plys, False); DrawTriangle(Canvas, AB, BC, B, Plys, False); end; end; end; begin ScreenColors; end.Отображение ландшафта может выполняться в трех режимах: каркасном (Outline), c заполнением (Filled) и со светотенью (rendered). В любом из этих режимов ландшафт рисуется как набор треугольников, при этом координаты отдельных вершин TTriple с помощью простого перспективного преобразования пересчитываются в экранные пиксели TPixel, а затем получившийся треугольник рисуется с помощью функции PolyLine или Polygon. Единственное отличие между режимами заключается в том, что в каркасном режиме рисуется обычная «проволочная сетка» без отсечения невидимых линий, а в двух последних режимах порядок вывода и заполнение прямоугольников обеспечивают отсечение невидимых линий методом «грубой силы» (иногда это называется «алгоритмом маляра»). В свою очередь режим со светотенью отличается тем, что цвет каждого треугольника в нем зависит от угла, под которым данная грань расположена по отношению к «солнцу».
Чтобы увеличить правдоподобие изображения, в Draw3Vertices() реализована упрощенная концепция «уровня моря». Любой треугольник, полностью находящийся над уровнем моря, рисуется нормально, а любой треугольник, полностью погруженный в воду, рисуется синим цветом на уровне моря. Если треугольник пересекает уровень моря, FL3 интерполирует точки пересечения, после чего отдельно рисует надводную и подводную части. Хотя для «побережий» такая методика вполне приемлема, с «озерами» дело обстоит сложнее: FL3 рисует воду лишь в тех местах, которые находятся ниже уровня моря.
После завершения прорисовки всех треугольников FL3 рисует вертикальные линии вдоль двух передних краев от уровня моря до всех вершин, которые находятся над водой. Эти линии особенно полезны в заполненном и светотеневом режимах — непрозрачные вертикальные грани будут скрывать «внутреннюю» структуру поверхности.
Гибкое кодирование
Многие продукты содержат специальные «точки входа» (hooks), через которые к ним можно подключить дополнительные модули, выпущенные независимыми фирмами. Например, в Windows Help определен интерфейс, с помощью которого разработчики могут включать в справочные файлы Windows нестандартные макросы и вспомогательные окна, добиваясь очень интересных эффектов. Интегрированная среда Borland C++5.0 также содержит интерфейс, с помощью которого в нее можно добавлять новые возможности. В комплект BC++ 5.0 входят модуль поддержки групповой разработки (контроля версий файлов) и дополнение для работы на Java, реализованные в виде DLL и подключенные через интерфейс расширения.
В этой главе я приводил пример с преобразованием форматов текстового редактора как один из возможных вариантов использования DLL. Давайте разовьем эту идею и напишем мини-редактор с интерфейсом расширения для таких преобразований. Сам редактор будет чрезвычайно простым — всего лишь компонент Memo с командами меню для открытия и сохранения файлов. Этого будет вполне достаточно, ведь в первую очередь нас интересует интерфейс форматных преобразований.
разрядные консольные приложения
Джим Мишель
Высушенное чучело DOS красуется ныне на стенке Win32 в качестве второстепенного API. Как же теперь бедному хакеру создать текстовый фильтр, запускаемый из командной строки? Добрая фея POSIX взмахивает волшебной палочкой… Дзынь! DOS на глазах превращает ся в консольное приложение, вызывая мучительное ощущение deja vu.
В течение многих лет Windows, OS/2, Macintosh и другие графические пользовательские интерфейсы (GUI) оставались излюбленной темой компьютерной прессы. Когда основное внимание уделяется разработке приложений для GUI, бывает трудно вспомнить о том, что существует и другой мир - мир средств командной строки, которые выполняют пакетные вычисления с минималь ным вводом информации от пользователя. Пусть такие программы выглядят не слишком эффектно - несомненно, они приносят немалую пользу. Скажем, банки обрабатывают сведения о ваших чеках, вкладах и ссудных платежах ночью, в пакетном режиме. Страховые и кредитные компании, а вместе с ними и другие бесчисленные учреждения тоже обновляют информацию по ночам. Нужны ли им для этого красивые среды GUI? Спросите своего кассира в банке. Или попробуйте угадать сами.
Возможности средств командной строки отнюдь не ограничиваются финансовыми расчетами на «крутом железе». Несколько таких программ входит в комплект Windows95, среди них - ATTRIB, DISKCOPY, FORMAT, FDISK, SORT и XCOPY. Они присутствуют даже в Delphi - при самом поверхностном просмотре каталога BIN там можно найти компиляторы ресурсов (BRC32.EXE и BRCC32.EXE), компилятор языка Паскаль (DCC32.EXE) и другие программы.
разрядные DLL в Delphi— когда, зачем и как
Джим Мишель
VCL-компоненты открывают новые возможности для многократного использования кода, но даже древние механизмы — такие как Windows DLL — при разумном применении способны творить чудеса.
Весна началась интересно. В феврале было холодно — здесь, в Остине, даже пошел снег. Дороги заледенели, машины разбивались буквально на каждом углу. Неплохое развлечение, если только в нем не участвует твоя машина. Вскоре после снегопада у нашего старенького «Бронко» забарахлил водяной насос и прохудился уплотнитель, и мы решили, что настало время подумать о новой машине. Вы не приценивались к так называемым «недорогим машинам»? Просто ужас!
Следующим вопросом на повестке дня оказался фильтр плавательного бассейна. В апреле у нас уже купаются, поэтому я открыл эту штуковину (какой странный оттенок зеленого…) и включил насос. Ни капли. Пришел спец по бассейнам и все исправил, но в итоге я стал заметно беднее. Потом засорилась система очистки воды, потому что идиот подрядчик сэкономил 20 долларов и поставил между домом и резервуаром ненадежную трубу. Водопроводчик содрал еще больше, чем спец по бассейнам. Короче, обитателям chez Mischel эти два месяца обошлись довольно дорого.
Я не прошу вашего сочувствия, а просто пытаюсь объяснить, что нельзя заранее предусмотреть всего, что может случиться, поэтому нужно проявлять гибкость, иначе цепочка несчастливых событий перевернет вашу жизнь вверх дном. То же самое относится и к программам — если вы не заложите в них определенную долю гибкости, это сделает кто-то другой, и в итоге вы лишитесь покупателей.
В жизни гибкость обычно обеспечивается денежными затратами. При программировании для Windows гибкость достигается с помощью DLL.
Перетаскивание: как это делается в Windows
Джим Мишель
С перетаскиванием в Windows дело обстоит сложнее, чем кажется на первый взгляд,— но если бы все было просто, кто стал бы читать книги по программированию?
Программы на Delphi поддерживают как минимум три разных интерфейса перетаскивания. В классе TControl, являющемся общим предком для всех управляющих элементов Delphi, определен межэлементный интерфейс перетаскивания. Включая в программу на Delphi обработчики для OnDragDrop, OnDragOver и других аналогичных событий, вы сможете наделить ее поддержкой внутренних операций перетаскивания. Если приложить некоторые усилия и использовать общую область памяти, метод можно расширить и организовать взаимодействие двух программ, написанных на Delphi. Тем не менее он не подойдет для перетаскивания между приложением, написанным на Delphi, и посторонней программой. Данный интерфейс наглядно поясняется документацией Delphi и программами-примерами.
Интерфейс перетаскивания также определен в OLE — интерфейсе связывания и внедрения Windows 1. Программы, написанные на Delphi, могут поддерживать этот интерфейс с помощью встроенных элементов OLE. Эти элементы позволяют построить клиентское или серверное приложение OLE, обладающее полноценной поддержкой перетаскивания OLE-объектов. В «чистой» Windows-программе нормально использовать OLE оказывается непросто. В Delphi существуют классы, которые поддерживают OLE и в некоторой степени облегчают OLE-программирование. В следующей главе я покажу, как реализовать перетаскивание средствами OLE с помощью таких классов.
1Когда-то сокращение OLE действительно расшифровывалось как Object Linking and Embedding, но сейчас рамки OLE значительно расширились, и сокращение официально признано самостоятельным термином. — Примеч. перев.
Третья разновидность перетаскивания, поддерживаемая в Delphi, — перетаскивание файлов из File Manager (Windows NT 3.5) или Windows Explorer (Windows 95 и NT 4.0). Этот интерфейс обладает минимальными возможностями (допускается лишь перетаскивание файлов), но оказывается на удивление полезным. Именно этот интерфейс, совершенно не упоминающийся в документации по Delphi, станет темой данной главы. Я использую для него термин FMDD (File Manager Drag and Drop).
Перетаскивание: как это делается вOLE
Джим Мишель
Оказывается, перетаскивание файлов из File Manager — всего лишь частный случай более общего интерфейса перетаскивания OLE. С помощью интерфейса OLE ваше приложение может превратиться в сервер перетаскивания, способный передавать другим приложени ям не только файлы, но и данные других типов.
Знания — забавная штука. Точнее, даже не сами знания, а то, как они нам достаются. Мне почти всегда приходится изучать что-то новое методом проб и ошибок, хотя, если бы у меня был выбор (или, возможно, всего лишь чуть лучшая подготовка), я бы охотно предпочел другой метод. Тот, кто попытает ся написать приложение с поддержкой перетаскивания OLE на основе скудной информации, содержащейся в документации OLE и Windows Software Development Kit (SDK), пройдет полноценный курс выживания в экстремаль ных условиях. Я могу предъявить шрамы, доказывающие справедливость этих слов.
Компонент Winsock в Delphi
Объекты хороши… но компоненты лучше. Чтобы наши программы могли мгновенно обращаться к Internet, мы упакуем весь багаж Winsock в один VCL-компонент.
Internet (и распределенные среды вообще) с каждым днем становится все популярнее, поэтому сетевая поддержка в приложениях выглядит вполне естественно. Lingua francaдля работы с Internet в Microsoft Windows является Winsock API. Описанный в этой главе компонент Winsock1 станет отправной точкой, позволяющей вам самостоятельно написать многие знакомые программы на базе TCP/IP— такие, как FINGER, FTP, SMTP, POP3 и ECHO.
CsShopper: FTP-клиент
Джон Пенман
Отправляйтесь в Internet за бесплатным барахлом! В этом вам поможет компонент, выполняющий функции FTP-клиента, и полноцен ное приложение для пересылки файлов, построенное на его основе.
Популярность Internet в немалой степени обусловлена возможностью обмена информацией между компьютерами. Такой обмен становится возможным благодаря протоколу пересылки файлов FTP (File Transfer Protocol)— одному из самых старых протоколов, используемых в Internet. Формальная спецификация используемого в настоящее время протокола FTP содержится в документе RFC959.
Протокол FTP, как и другие Internet-протоколы, берет свое начало в классической модели клиент/сервер. FTP-сервер иногда представляется мне в виде старомодного продавца, который снимает товар с полки и передает его покупателю (FTP-клиенту). В этой главе мы реализуем компонент Delphi с весьма подходящим именем CsShopper, выполняющий функции FTP-клиента.
Компонент CsShopper построен на основе CsSocket — простейшего компонента-оболочки для функций Winsock API, созданного в главе 5. CsSocket обеспечивает базовые возможности, необходимые для работы протокола FTP в сети TCP/IP. Таким образом, о мелочах есть кому позаботиться, и мы можем сразу же прейти к более пристальному рассмотрению процесса FTP глазами клиента.
FTP-сервер
Джон Пенман
Как известно, в FTP участвуют две стороны. Создание нестандартного компонента, выполняющего функции FTP-сервера, позволит вам полностью контролировать операции пересылки файлов между Internet-приложениями.
В главе6 я описал компонент CsShopper, в котором инкапсулируются функции клиентской стороны при пересылке файлов с использованием протокола FTP. Более того, компонент, выполняющий функции FTP-клиента, даже входит в число примеров Delphi 3. И все же для осуществления полноценного обмена файлами недостаточно иметь только клиентское приложение. Сейчас в Сети появляется все больше пользователей с круглосуточным доступом (за которым закрепился термин 24?7), и все больше людей желает создавать на Delphi свои собственные программы-серверы. Итак, знакомьтесь — CsKeeper!
CsKeeper — потомок компонента CsSocket из главы 5. В этом VCL-компоненте инкапсулируется серверная сторона FTP-протокола. CsKeeper чем-то похож на продавца маленького магазинчика — он «берет с полки» те файлы, которые затребованы, и передает их клиенту «через прилавок». Впрочем, в отличие от продавца сервер является конечным автоматом, строго соблюдающим правила протокола FTP (и к тому же не пытается болтать на посторонние темы).
Большая часть того, что было сказано о компоненте CsShopper в главе 6, относится и к CsKeeper. Если вы еще не читали главу 6, я настоятельно вам рекомендую начать именно с нее. В сложном танце под аккомпанемент FTP-протокола участвуют две стороны, и понимание одной из них невозможно без определенного понимания другой.
Если вы считаете, что достаточно хорошо разобрались с клиентской стороной, мы можем продолжать. Сервер FTP обычно ожидает установки клиентского соединения на TCP-порте с номером 21. При соединении сервер инициирует процесс регистрации, посылая клиенту команду USER. Поскольку процесс регистрации был достаточно подробно рассмотрен в главе 6 при описании CsShopper, я не стану задерживаться на его подробностях. После успешной регистрации сервер готов к выполнению любого FTP-запроса, поступившего от клиента. Магазин открылся! К тому что происходит дальше, стоит присмотреться повнимательнее.
В компоненте CsKeeper воплощен простой и полезный FTP-сервер, который соответствует минимальным требованиям, формально изложенным в документе RFC959. Следовательно, некоторые команды FTP (такие как ACCT, NLIST и PASV) в настоящее время отсутствуют в словаре CsKeeper. В таблице 7.1 приведен список всех FTP-команд. Команды, не реализованные в текущей версии CsKeeper, помечены звездочкой. При получении неподдерживаемой команды CsKeeper возвращает клиенту код ошибки с содержательным сообщением.
Обратите внимание: CsKeeper не является FTP-сервером с параллельной обработкой. Это означает, что в каждый момент времени он может обслужи вать лишь одного пользователя.
Таблица 7.1. Набор команд FTP
ABOR ACCT* ALLO* APPE* CDUP CWD DELE HELP LIST MKD MODE NLIST* NOOP PASS PASV* PORT PWD QUIT REIN* RMD RNFR* RNTO* REST* RETR SITE SMNT* STAT* STOR STOU* STRU* SYST TYPE USER* |
Прерывание текущей пересылки файла Передача информации о ресурсах пользователя Выделение места под новый файл Добавление данных в существующий файл Переход в родительский каталог Переход в другой каталог Удаление файла, выбранного пользователем Запрос справочной информации о FTP-команде Запрос списка файлов текущего каталога Создание нового каталога Использование режима пересылки, выбранного клиентом Запрос потока с именами файлов Передача сервером ответа «OK» Передача пароля во время регистрации Прослушивание сервером конкретного порта данных Использование сервером порта данных, выбранного клиентом Запрос имени текущего каталога Завершение FTP-сеанса Повторная инициализация сеанса Удаление каталога Передача имени файла, который следует переименовать Передача нового имени файла. Команда должна передаваться после RNFR Возобновление прерванной пересылки файла Получение файла с сервера Получение информации о специфических услугах сервера Монтирование другой файловой системы на сервере Запрос информации о статусе Запрос на сохранение файла Сохранение файла с уникальным именем на сервере Запрос на использование файловой структуры, выбранной клиентом Запрос типа операционной системы Выбор типа пересылаемого файла Передача имени пользователя во время регистрации команда не реализована в текущей версии CsKeeper |
Главный секрет иерархий
При работе с иерархиями используется «семейная» терминология (родители, внуки, предки, потомки), поскольку семья является самым распространенным примером объектов (в данном случае — людей), объединенных иерархиче скими отношениями. Этот пример напомнит вам одну простую истину — хотя вы можете построить систему, предназначенную для обобщенной обработки рекурсивных иерархий, ценность каждого объекта определяется той уникальной информацией, которая в нем хранится. В то же время место объекта в иерархическом дереве — не более чем условное обозначение связи с другими объектами. Иерархическая структура всего лишь помогает сохранить и найти объект; ваша задача — сделать так, чтобы этот объект оправдал затрачен ные усилия.
Глобальный доступ к данным в приложении
Тем временем Мститель снова погрузился в чтение похищенного Дневника.
Дневник №16 (27 марта). В Delphi 1.0 совместное использование таблиц несколькими формами было крайне хлопотным делом. Хотя возможности не ограничивались размещением таблиц и источников данных на всех формах, работавших с данными, неуклюжее альтернативное решение требовало временного создания дополнительных источников данных с их последующим удалением. Мне часто хотелось отыскать более простой способ. В последую щих версиях Delphi появились объекты, которые назывались модулями данных и заметно упрощали эту задачу. Я решил побольше узнать о них.
Как выяснилось, модуль данных представляет собой специализированную форму, на которой можно разместить только стандартные объекты из палитры Data Access. В приложении можно построить целую базу данных, с таблицами, источниками, запросами и всем остальным— и разместить ее в одном модуле данных. Чтобы результатами трудов могли воспользоваться другие формы, необходимо включить модуль данных в их секции implementation (не в секции interface!). При этом компоненты и поля модуля данных становятся доступными для компонентов формы, связанных с данными (а также для самого инспектора объектов).
Я решил создать простой пример с данными одного из моих клиентов, фирмы «Чичен-Итца Пицца». Данные хранятся в виде таблицы Paradox, в файле PIZADAT.DB. Таблица состоит из трех полей: название, цена продажи и себестоимость лучших продуктов фирмы. Я решил добавить вычисляемое поле для отображения прибыли по каждой позиции (в процентах).
Рис. 15.6. Модуль данных в режиме конструирования
Сначала я создал (для последующего использования в свойстве DatabaseName) псевдоним с именем Pizza, определяющий каталог с таблицей. Затем создал новый модуль данных и присвоил ему имя PizzaData. В этот модуль (см. рис. 15.6) я поместил таблицу и источник данных, присвоив им имена ProductTable и ProductSource соответственно. Я подключил ProductSource
к ProductTable и задал свойству AutoEdit значение False. Затем открыл для таблицы Fields Editor и добавил в него все возможные поля. Наконец, я создал новое вычисляемое поле для хранения процента прибыли и написал обработ чик события OnCalcFields таблицы ProductTable. Окно модуля данных показано на рис. 15.6. Исходный текст модуля PizzaData приведен в листинге 15.4.
Листинг 15.4. Исходный текст модуля данных
{——————————} {Демонстрация работы с модулями данных } {PIZADAT.PAS : Модуль данных } {Автор: Эйс Брейкпойнт, N.T.P. } {При содействии Дона Тейлора } { } { Модуль данных содержит простейшую комбинацию } { таблица/источник данных,подключаемую к таблице} { Paradox. Для пользователей модуля создано } { вычисляемое поле. } { } { Написано для *High Performance Delphi 3 } Programming* } { Copyright (c) 1997 The Coriolis Group, Inc.} { Дата последней редакции 23/4/97 } {—————————} unit PizaDat; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, DB, DBTables; type TPizzaData = class(TDataModule) ProductTable: TTable; ProductSource: TDataSource; ProductTableName: TStringField; ProductTablePrice: TCurrencyField; ProductTableCost: TCurrencyField; ProductTablePctProfit: TFloatField; procedure ProductTableCalcFields(DataSet: TDataSet); private { Private declarations } public { Public declarations } end; var PizzaData: TPizzaData; implementation {$R *.DFM} procedure TPizzaData.ProductTableCalcFields (DataSet: TDataSet); begin ProductTablePctProfit.Value := 100.0 * ((ProductTablePrice.Value - ProductTableCost.Value) / ProductTableCost.Value); end; end.С данными все понятно. Настало время писать демонстрационную
программу. Я решил поиграть с двумя формами. Первая форма просто обращается к данным через источник, расположенный в модуле данных. Эта фор ма отображает имя продукта и вычисляемое поле. Я поместил на нее компонент-навигатор для перемещения по таблице.
На второй (главной) форме приложения находятся собственные компонен ты таблицы и источника данных, а также два других компонента: сетка TDBGrid и навигатор. Кроме того, я поместил на нее группу переключателей, позволяющих динамически переключаться между модулем данных и локальным источником. При выборе локального источника данных навигаторы на обеих формах (см. рис. 15.7) работают независимо, поскольку в них используются разные объекты-таблицы.
На рис. 15.7 изображены обе формы во время работы. В листинге 15.5 приведен исходный текст главной формы, а в листинге 15.6 — исходный текст вспомогательной формы.
Рис. 15.7. Программа, демонстрирующая использование модуля данных
Листинг 15.5. Исходный текст главной формы
{——————————————————————————————————————————————————————} { Демонстрация работы с модулями данных } { PIZAMAIN.PAS : Главная форма } { Автор: Эйс Брейкпойнт, N.T.P. } { При содействии Дона Тейлора } { } { Демонстрационная программа показывает, как } { происходит подключение формы к модулю данных, } { созданному для данного проекта. Форма } { содержит переключатель для смены источника данных - } { модуль или локальная пара таблица/источник данных. } { } { Написано для *High Performance Delphi 3 Programming* } { Copyright (c) 1997 The Coriolis Group, Inc. } { Дата последней редакции 23/4/97 } {——————————————————————————————————————————————————————} unit PizaMain; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, Grids, DBGrids, ExtCtrls, DBCtrls, DBTables, DB, StdCtrls; type TForm1 = class(TForm) DBGrid: TDBGrid; Navigator: TDBNavigator; DataSourceRBGroup: TRadioGroup; QuitBtn: TButton; LocalTable: TTable; LocalDataSource: TDataSource; LocalTableName: TStringField; LocalTablePrice: TCurrencyField; LocalTableCost: TCurrencyField; Bevel1: TBevel; procedure FormShow(Sender: TObject); procedure FormCreate(Sender: TObject); procedure DataSourceRBGroupClick(Sender: TObject); procedure QuitBtnClick(Sender: TObject); private { Private declarations } public { Public declarations } end; var Form1: TForm1; implementation uses PizaDat, PizaFrm2; {$R *.DFM} procedure TForm1.FormShow(Sender: TObject); begin Form2.Show; end; procedure TForm1.FormCreate(Sender: TObject); begin DataSourceRBGroup.ItemIndex := 0; end; procedure TForm1.DataSourceRBGroupClick(Sender: TObject); begin if Tag > 0 then case DataSourceRBGroup.ItemIndex of 0 : begin DBGrid.DataSource := PizzaData.ProductSource; Navigator.DataSource := PizzaData.ProductSource; end; 1 : begin DBGrid.DataSource := LocalDataSource; Navigator.DataSource := LocalDataSource; end; end { case } else Tag := 1; end; procedure TForm1.QuitBtnClick(Sender: TObject); begin Close; end; end.Листинг 15.6. Исходный текст вспомогательной формы
{——————————————————————————————————————————————————————} { Демонстрация работы с модулями данных } { PIZAFRM2.PAS : Вспомогательная форма } { Автор: Эйс Брейкпойнт, N.T.P. } { При содействии Дона Тейлора } { } { Демонстрационная программа показывает, как } { происходит подключение формы к модулю данных, } { включенному в проект. Эта форма получает данные } { из модуля данных проекта. } { } { Написано для *High Performance Delphi 3 Programming* } { Copyright (c) 1997 The Coriolis Group, Inc. } { Дата последней редакции 23/4/97 } {——————————————————————————————————————————————————————} unit PizaFrm2; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, DBCtrls, ExtCtrls, DB; type TForm2 = class(TForm) NameDBText: TDBText; PctDBText: TDBText; Label1: TLabel; Label2: TLabel; Navigator: TDBNavigator; Bevel1: TBevel; private { Private declarations } public { Public declarations } end; var Form2: TForm2; implementation uses PizaDat; {$R *.DFM} end.Перед компиляцией я задаю свойствам Active всех объектов-таблиц значение True. Наверное, сказывается сила привычки.
В секциях implementation обеих форм указывается PizaDat (имя модуля данных). После этого поля модуля данных становятся доступными в инспекторе объектов для любого компонента, связанного с данными.
Как видно из листинга, переключение между двумя источниками данных с помощью переключателей не вызывает никаких проблем. Вернее, почти не вызывает…
Похоже, обработчик OnClick для переключателей вызывается во время создания формы. К этому моменту источники данных еще не были полностью сконструированы и подключены, и в результате возникает исключение. Я решил предотвратить эту ситуацию с помощью свойства Tag формы. Во время инициализации оно равно 0, поэтому попытка подключения источников не производится. Однако следующий вызов обработчика — совсем другое дело, поскольку значение Tag было заменено на 1.
Демонстрационная программа работает именно так, как я планировал. Когда главная форма подключается к модулю данных, на ней отображается процент прибыли, и обе формы синхронизированы независимо от того, какая из них управляет перемещением по таблице. Когда главная форма переключается на локальный источник данных, обе формы начинают действовать независимо. Обратное переключение на модуль данных мгновенно синхрони зирует их.
Разумеется, возможности модулей данных отнюдь не ограничиваются вычисляемыми полями. Ведь в конце концов модуль данных является полноценным модулем Object Pascal, который может содержать новые объекты и методы, а также обработчики для любых событий, связанных с таблицами, источниками данных, SQL-запросами и т.д. В сущности, программист может реализовать полный набор логических правил для работы с данными компании. Довольно круто — и открывает очень, очень широкие возможности.
Конец записи (27 марта).
Группы переключателей с индивидуальной блокировкой
Ничто так не радует во время конструирования форм, как элементы, которые автоматически выравниваются, масштабируются и выстраивают свое содержимое в аккуратные столбики. Возникает впечатление, будто у вас появились надежные союзники. Однако достоинства «умных» элементов вовсе не исчерпываются психологическим комфортом— подумайте, сколько строк программного кода вам сэкономило свойство Align панелей? Десятки, сотни? Теперь вы понимаете, почему мне так не хочется отказываться от удобного элемента TRadioGroup, когда возникает необходимость в блокировке отдельных переключателей. Класс TRadioGroup автоматически располагает переключатели в виде столбцов, выравнивает расстояния между ними и позволяет задать их имена в виде одного строкового списка.
Однако он не позволяет обращаться к отдельным переключателям группы — и наверняка для этого есть веские причины. Но я уверен в своей способности разумно блокировать тот или иной переключатель и поэтому написал улучшенный вариант TRadioGroup (см. листинг 9.12). Класс TRadioBtnGrp содержит новое свойство ItemEnabled, с помощью которого можно получать и задавать состояние блокировки для отдельных кнопок.
Листинг 9.12. Модуль RBTNGRPS.PAS
{ Группа переключателей с возможностью блокировки отдельных кнопок } unit RBtnGrps; interface uses StdCtrls, ExtCtrls, Classes; type TRadioBtnGroup = class( TRadioGroup ) private function GetItemEnabled( Index: Integer ) : Boolean; procedure SetItemEnabled( Index: Integer; Value: Boolean ); function GetButtons( Index: Integer ) : TRadioButton; protected function CheckAnyBut( NotThisIndex: Integer ): Boolean; property Buttons[ Index: Integer ] : TRadioButton read GetButtons; public property ItemEnabled[ Index: Integer ] : Boolean read GetItemEnabled write SetItemEnabled; end; procedure Register; implementation function TRadioBtnGroup.CheckAnyBut; var Index: Integer; begin Result := True; for Index := NotThisIndex + 1 to Items.Count - 1 do if Buttons[ Index ].Enabled then begin Buttons[ Index ].Checked := True; Exit; end; for Index := 0 to NotThisIndex - 1 do if Buttons[ Index ].Enabled then begin Buttons[ Index ].Checked := True; Exit; end; Result := False; end; function TRadioBtnGroup.GetItemEnabled; begin Result := Buttons[ Index ].Enabled; end; procedure TRadioBtnGroup.SetItemEnabled; begin if ( not Value ) and ( Index = ItemIndex ) and Buttons[ Index ].Checked and ( not CheckAnyBut( Index )) then ItemIndex := -1; Buttons[ Index ].Enabled := Value; end; function TRadioBtnGroup.GetButtons; begin Result := Components[ Index ] as TRadioButton; end; procedure Register; begin RegisterComponents('HP Delphi 3', [ TRadioBtnGroup ]); end; end.Во внутренней реализации TRadioBtnGroup метод GetButtons используется для получения доступа к отдельным переключателям. GetButtons использует тот факт, что входящие в группу переключатели хранятся в массиве Components. Все, что требуется от GetButtons — индексировать массив Components и выполнить безопасное преобразование типа для результата.
Новый элемент стремится работать как можно разумнее. При блокировке установленного переключателя он пытается установить другой переключатель; если заблокированы все переключатели, он ничего не устанавливает. Если такое поведение вас не устраивает, его можно изменить.
Hello, Delphi
Прежде всего создайте новое приложение (File <>New Application). Для начала нужно изменить некоторые параметры проекта и сообщить Delphi о том, что мы создаем именно консольное приложение. Выполните команду Projectд Options и затем на вкладке Linker диалогового окна Project Options установите флажок Generate Console Application, после чего сохраните внесенные изменения кнопкой OK.
Поскольку у консольного приложения нет главной формы (и, если уж на то пошло, вообще никаких форм), необходимо удалить форму Form1, которая автоматически появилась при создании нового приложения. Выполните команду FileдRemove From Project; когда появится диалоговое окно Remove From Project, выделите строку, содержащую имена Unit1 и Form1, и нажмите кнопкуOK. Если откроется окно сообщения с предложением сохранить изменения в модуле Unit1, нажмите кнопку No. В оставшемся окне Delphi нет ничего, кроме инспектора объектов, - нет ни форм, ни модулей. Где же писать код программы?
Остается лишь файл с исходным текстом проекта. Выполните команду ViewдProject Source. Delphi откроет окно текстового редактора с файлом PROJECT1.DPR. Именно этот файл мы модифицируем, чтобы создать первое консольное приложение. Перед тем как продолжать работу над программой, выполните команду File <>Save и сохраните проект под именем HELLO.DPR.
В редакторе измените исходный текст проекта в соответствии с листингом 1.1 и сохраните свою работу. Нажмите клавишу F9, чтобы откомпилировать и запустить программу.
Листинг 1.1. Программа Hello, Delphi
{ HELLO.DPR - Простейшее консольное приложение Delphi Автор: Джим Мишель Дата последней редакции: 04/05/97 } {$APPTYPE CONSOLE} program Hello; uses Windows; begin WriteLn ("Hello, Delphi"); Write ("Press Enter..."); ReadLn; end.Строка {$APPTYPE CONSOLE} в листинге 1.1 является директивой компилято ра и сообщает Delphi о том, что создаваемое приложение является консольным. Она должна присутствовать в начале любого консольного приложения. Эта директива включается только в программы - она не нужна в модулях или библиотеках динамической компоновки (DLL). Ключевое слово uses нашей программе, вообще говоря, не нужно (мы здесь не обращаемся к функциям Windows API), но по какой-то загадочной причине Delphi не любит сохранять проекты без секции uses (см. мое замечание о методе проб и ошибок). Включение модуля Windows не принесет никакого вреда и говорит вовсе не о том, что модуль подключается к программе, а лишь о том, что Delphi просмотрит его, если не сможет найти какой-нибудь идентификатор в текущем модуле.
Оставшаяся часть программы проста до очевидного. Строка «Hello, Delphi» выводится на консоль (то есть на экран), после чего вам будет предложено нажать Enter. Я включил сюда ожидание ввода лишь потому, что без него Delphi на долю секунды выведет окно консоли (сеанса DOS), запустит программу и сразу же закроет окно. Ожидание нажатия Enter позволяет убедиться в том, что программа действительно работает.
Хочу быть сервером!
С приемником у меня не было особых проблем — стоило понять общую концепцию интерфейса COM, и дальше все прошло относительно безболезнен но. Построение сервера, напротив, сопровождалось сплошными неудачами. На первых порах казалось, что мне придется реализовать всю «кухню» перетаскивания лишь для того, чтобы наладить работу простейшего сервера. Чтобы создать сервер перетаскивания, необходимо реализовать три интерфейса, причем ни один из них нельзя протестировать до того, как будут готовы остальные. В результате при отладке создается занятная ситуация — совершенно непонятно, в какой же части программы возникает проблема.
Замечание
Конечно, мои трудности отчасти были обусловлены недостатком опыта работы с OLE и COM, но я твердо убежден в том, что больше всего проблем вызвали излишняя сложность интерфейса и совершенно неудовлетворительная документация. Я достаточно хорошо владею C и C++, так что меня уже не пугает документация Windows SDK, качество которой варьируется от нулевого до условно-полезного. С другой стороны, примеры из SDK не назовешь понятными или полезными даже для опытного программиста на C++. Вместо изощренных примеров OLE, которые пытаются объяснить все сразу и в итоге не объясняют толком ничего, гораздо больше пользы принесли бы простые программы, просто и наглядно поясняющие конкретные концепции. Изучение файла OLECTNRS.PAS (из каталога Delphi Source\VCL) дало мне больше, чем все примеры Microsoft SDK.
И последнее замечание…
В листинге16.2 реализована еще одна дополнительная возможность, перед которой я не смог устоять. Объект можно перетащить из сетки и скопировать /переместить его в другую сетку, сбрасывая на нужном корешке. Для этого мне пришлось написать общий обработчик OnMouseDown для всех сеток, а также расширить обработчики OnDragOver и OnDragDrop для компонента PageControl. Кроме того, я добавил флаг CopyDrag, устанавливаемый в том случае, если в начале перетаскивания из любой сетки была нажата клавиша Ctrl.
При перетаскивании из сетки на корешок вкладки основную долю работы выполняет процедура DropGridString. Если во время перетаскивания не была нажата клавиша Ctrl, DropGridStringвыполняет дополнительные действия и превращает обычное копирование в перемещение, убирая выделенный объект из сетки-источника и затем удаляя пустую строку.
?абочая версия программы изображена на рис. 16.2. Это маленькое приложение получилось довольно забавным. Вы можете перетаскивать объекты между вкладками, копировать и перемещать их. Это гораздо веселее, чем сидеть на свадьбе (особенно на своей собственной).
Конец записи (29 марта).
?ис. 16.2. Общие обработчики событий в действии
Факс в конторе Эйса зажужжал. Хелен немедленно вскочила на ноги.
— Эйс, пришел факс, — сказала она. — Поторопись, это должны быть результаты экспертизы.
Брейкпойнт пересек комнату и оторвал листок.
— Посмотрим, кто из нас прав и действительно ли это дело рук Бохакера.
Он застыл на месте, несколько секунд молча разглядывая страницу. Наконец Хелен потеряла терпение.
— Ну, что там написано? — потребовала она. — Это Бохакер, да?
— Видишь ли, не совсем понятно. Такая быстрая экспертиза не всегда дает однозначный ответ, и…
— Дай посмотреть, — сказала Хелен и отняла листок. Быстро пробежав его глазами, она повернулась к своему компаньону.
— Здесь ясно написано — цитирую: «Экспертиза показала практически полное совпадение обоих образцов с погрешностью до 5 процентов, что соответствует погрешности, допустимой при экспертизе такого рода». Это означает, что образцы крови и волос совпали, не так ли?
— В общем, да, — признал Эйс. — Но…
— Значит, это должен быть Мелвин Бохакер, как я и говорила. И где бы он ни был, наверняка рядом с ним находится и Мадам Икс. Остается лишь узнать, где они.
— В Нортон-Сити.
— Что?
— В Нортон-Сити, — повторил Эйс. — Бифф сообщил мне, что Бохакер уехал в Нортон-Сити. Уж можешь мне поверить.
— Но где именно? — спросила она. — Он может находиться в сотне мест.
— Кажется, я знаю, как это выяснить, — сказал Эйс и включил компьютер.
Иерархические структуры вреляционных базах данных
Ричард Хейвен
Данные не всегда удается представить в виде таблицы, состоящей из строк и столбцов. В этой главе приведены рекомендации по работе с иерархическими структурами в базах данных Delphi и описаны некоторые VCL-компоненты, снимающие с вас часть забот.
Окружающий мир переполнен иерархическими данными. В это широкое понятие входят компании, состоящие из дочерних компаний, филиалов, отделов и рабочих групп; детали, из которых собираются узлы, входящие затем в механизмы; специальности, специализации и рабочие навыки; начальники и подчиненные и т. д. Любая группа объектов, в которой один объект может быть «родителем» для произвольного числа других объектов, организована в виде иерархического дерева. Очевидным примером может послужить иерархия объектов VCL — класс TEdit представляет собой частный случай TControl, потому что TControl является его предком. С другой стороны, TEdit можно рассматривать и как потомка TWinControl или TCustomControl, потому что эти классы являются промежуточными уровнями иерархии VCL.
Подобные связи не имеют интуитивного представления в рамках модели реляционных баз данных. Нередко иерархические связи являются рекурсив ными (поскольку любая запись может принадлежать любой записи) и произвольными (любая запись может принадлежать другой записи независимо от того, кому принадлежит последняя). В двумерной таблице даже отображение иерархического дерева становится непростым делом, не говоря уже о запросах. Иногда в критерий запроса входит родословная (lineage) объекта (то есть его родители, родители его родителей и т. д.) или его потомство (progeny — сюда входят дочерние объекты и все их потомство). В этой главе описаны некоторые механизмы работы с иерархическими связями в модели реляционных баз данных, хорошо знакомой программистам на Delphi.
Иерархия «один-ко-многим»
Delphi обладает удобными средствами для работы с реляционными базами данных. Такие базы данных представляют собой таблицы (иногда также называемые отношениями— relations), состоящие из строк (записей) и столбцов (полей), которые связываются друг с другом по совпадающим значениям полей (см. рис. 13.1). В теории баз данных используются и другие представ ления. До появления реляционной модели стандартными были иерархическая (hierarchical) и сетевая (network) модели, а сейчас появился еще один тип — объектно-ориентированные (object-oriented) базы данных.
Рис. 13.1. Базовая и подчиненная таблицы
Любую модель следует оценивать по тому, насколько она облегчает труд разработчика при создании базы данных. Реляционная модель хорошо подходит для многих реальных структур данных: нескольких счетов для одного клиента, нескольких деталей для нескольких поставщиков, нескольких объектов с несколькими характеристиками, и т. д. С помощью свойств TTable.MasterSource и TQuery.DataSource можно выделить из таблицы некоторое подмножество записей или построить запрос, основанный на связанных значениях полей из другой таблицы. Это один из способов установить отношение между базовой (master) и подчиненной (detail) таблицами, где из базовой таблицы берется одна запись, а из подчиненной — несколько.
Интерфейс IDataObject хранит данные
Интерфейс IDataObject управляет содержанием перетаскиваемых данных, а также представлением их в формате, понятном для запрашивающего объекта IDropTarget. Он используется при перетаскивании, а также при обмене данными с буфером (clipboard). После того как вы наладите работу интерфейса IDataObject с первым типом передачи данных, со вторым особых проблем не возникнет. Впрочем, трудность (как правило) заключается в том, чтобы заставить IDataObject работать хотя бы в одном варианте. И что еще хуже, возникающие проблемы оказываются на редкость изощренными.
Интерфейс IDataObject предоставляет средства для передачи данных и сообщений об изменениях. Его методы предназначены для занесения данных в объект, представления их в различных (как правило, зависящих от конкретного устройства) форматах, возврата информации о поддерживаемых форматах и уведомления других объектов об изменении данных. Хотя для перетаскивания файлов в окно Windows Explorer или File Manager необходимо полностью реализовать лишь три метода IDataObject, в совокупности эти три метода оказываются весьма объемными.
Итак, чтобы создать сервер для перетаскивания файлов, нужно реализовать три метода IDataObject: QueryGetData, GetData и EnumFormatEtc. А чтобы реализовать метод EnumFormatEtc, понадобится реализовать и интерфейс IEnumFormatEtc. Я же говорил, что с реализацией интерфейсов все стремительно усложняется.
Метод QueryGetData вызывается приемником перетаскивания. Ему передается структура TFormatEtc, которая описывает формат данных, желательный для приемника. QueryGetData должен сообщить приемнику о том, может ли объект представить данные в требуемом формате. Он возвращает S_OK в том случае, если последующий вызов GetData с большой долей вероятности закончится успешно. В некоторых случаях (например, при нехватке памяти) последующий вызов GetData все равно может закончиться неудачей.
Когда приемник хочет получить данные, он вызывает метод GetData. Приемник передает структуру TFormatEtc с описанием желательного формата данных и структуру TStgMedium, в которую GetData поместит запрашиваемые данные. Вызывающая сторона (то есть приемник) должна освободить структуру TStgMedium после того, как обработка данных будет завершена. Этот момент чрезвычайно важен. Поскольку клиент уничтожает данные, возвращаемые GetData, метод должен передавать копию данных объекта. Если GetData передаст настоящие данные, клиент благополучно уничтожит их, и следующая попытка клиента (или самого объекта-источника) обратиться к данным приведет к катастрофе.
Метод EnumFormatEtc сообщает о том, в каких форматах объект может воспроизвести свои данные. Информация передается в виде объекта IEnum FormatEtc, а это означает, что для реализации IDataObject нам придется реализовать и интерфейс IEnumFormatEtc. Среди примеров OLE SDK приведено немало реализаций IEnumFormatEtc— но все они написаны на C или C++ и выглядят, мягко говоря, устрашающе. К счастью, классы TOleForm и TOleContainer из OLECNTNRS.PAS содержат более простой вариант, которым я воспользовался как шаблоном для своей реализации. После этого примеры IEnumFormatEtc из OLE SDK начали обретать для меня смысл, но без OLECTRNS.PAS я бы до сих пор рвал на себе волосы от отчаяния.
Интерфейс IEnumFormatEtc содержит четыре метода: Next, Skip, Reset и Clone, с помощью которых приложения могут перебирать и просматривать поддержи ваемые форматы данных, а также копировать список этих форматов. Универсальная реализация IEnumFormatEtc выглядит очень сложно, поскольку она должна уметь динамически выделять память под структуры TFormatEtc и копировать внутренние данные, содержащиеся в этих структурах. Такие сложности нам не нужны, поэтому предполагается, что рабочий массив TFormatEtc содержит статические данные. Для наших целей сойдет и так, но во многих приложениях это условие приведет к излишне строгим ограничениям. Предлагае мая реализация IEnumFormatEtc приведена в листинге 4.3.
Листинг 4.3. ENUMFMT.PAS: простейшая реализация интерфейса IEnumFormatEtc
{
ENUMFMT.PAS -- реализация интерфейса IEnumFormatEtc.
Автор: Джим Мишель
Дата последней редакции: 30/05/97
Приведенная реализация IEnumFormatEtc недостаточно надежна.
Она предполагает, что список FormatList, поддерживаемый объектом
TEnumFormatEtc, хранится в виде статического массива. Для простых
объектов наподобие сервера для перетаскивания файлов этого достаточно, но во многих
приложениях такое ограничение оказывается неприемлемым.
} unit EnumFmt; interface uses Windows, ActiveX; type { TFormatList -- массив записей TFormatEtc } PFormatList = ^TFormatList; TFormatList = array[0..1] of TFormatEtc; TEnumFormatEtc = class (TInterfacedObject, IEnumFormatEtc) private FFormatList: PFormatList; FFormatCount: Integer; FIndex: Integer; public constructor Create (FormatList: PFormatList; FormatCount, Index: Integer); { IEnumFormatEtc } function Next (celt: Longint; out elt; pceltFetched: PLongint): HResult; stdcall; function Skip (celt: Longint) : HResult; stdcall; function Reset : HResult; stdcall; function Clone (out enum : IEnumFormatEtc) : HResult; stdcall; end; implementation constructor TEnumFormatEtc.Create ( FormatList: PFormatList; FormatCount, Index : Integer ); begin inherited Create; FFormatList := FormatList; FFormatCount := FormatCount; FIndex := Index; end; { Next извлекает заданное количество структур TFormatEtc в передаваемый массив elt. Извлекается celt элементов, начиная с текущей позиции в списке. } function TEnumFormatEtc.Next ( celt: Longint; out elt; pceltFetched: PLongint ): HResult; var i : Integer; eltout : TFormatList absolute elt; begin i := 0; while (i < celt) and (FIndex < FFormatCount) do begin eltout[i] := FFormatList[FIndex]; Inc (FIndex); Inc (i); end; if (pceltFetched <> nil) then pceltFetched^ := i; if (I = celt) then Result := S_OK else Result := S_FALSE; end; { Skip пропускает celt элементов списка, устанавливая текущую позицию на (CurrentPointer + celt) или на конец списка в случае переполнения. } function TEnumFormatEtc.Skip ( celt: Longint ): HResult; begin if (celt <= FFormatCount - FIndex) then begin FIndex := FIndex + celt; Result := S_OK; end else begin FIndex := FFormatCount; Result := S_FALSE; end; end; { Reset устанавливает указатель текущей позиции на начало списка } function TEnumFormatEtc.Reset: HResult; begin FIndex := 0; Result := S_OK; end; { Clone копирует список структур } function TEnumFormatEtc.Clone ( out enum: IEnumFormatEtc ): HResult; begin enum := TEnumFormatEtc.Create (FFormatList, FFormatCount, FIndex); Result := S_OK; end; end.Интерфейсные формы
Когда я занялся реализацией интерфейсов из листинга 10.3, неожидан новозникли проблемы — моя система «зависала» при каждом вызове AddNotifiee(Self) из формы, реализующей IFrame. Хотя решение оказалось простым, мне пришлось в течение многих часов изобретать и проверять различные гипотезы. Чтобы вы лучше поняли суть происходившего, потребуется некоторая дополнительная информация.
Документация Delphi 3 достаточно четко объясняет, что каждый объект, реализующий какой-то интерфейс, должен также реализовать интерфейс IUnknown, в котором производятся подсчет ссылок и запросы поддерживаемых интерфейсов. Если компилятор встретит следующее объявление:
type IFoo = interface procedure Foo; end; TFoo = class (TObject, IFoo) procedure Foo; end;procedure TFoo.Foo; begin end;
он пожалуется на наличие необъявленных идентификаторов QueryInterface, _AddRef и _Release. Вам придется явным образом реализовать IUnknown или создавать свой объект на базе TInterfaced, а не TObject. С другой стороны, следующий фрагмент не вызовет у компилятора никаких проблем:
type
TFoo = class (TForm, IFoo) procedure Foo; end; procedure TFoo.Foo; begin end;
Это означает, что фирма Borland реализовала IUnknown где-то в недрах VCL и у нас стало одной заботой меньше, не так ли?
Нет, не так. При передаче TForm в качестве интерфейсной ссылки VCL выдает ошибку защиты (GPF). Хотя класс TComponent и реализует методы IUnknown, это вряд ли поможет тем из нас, кто захочет воспользоваться интерфейсами в приложении. Вызовы IUnknown передаются FVCLComObject — указателю, значение которого задается лишь при вызове GetComObject для получения интерфейсной ссылки объекта. Более того, GetComObject задает значение FVCLComObject лишь в том случае, если вы использовали VCLCom в своем проекте. Если сделать это, GetComObject начинает жаловаться на то, что фабрика класса (class factory) не была зарегистрирована, и… на этом я прекратил свои исследования. Возможно, все это очень здорово, если вы собираетесь использовать COM-объекты совместно с другими приложениями, но совершенно не подходит, если нужно всего лишь добавить интерфейсы к формам.
Намного проще будет заглянуть в реализацию TInterfacedObject и включить в TForm простую, независимую реализацию IUnknown, а затем порождать формы от TInterfacedForm вместо TForm.
Листинг 10.3. Модуль INTERFACEDFORMS.PAS
unit InterfacedForms; // Copyright © 1997 by Jon //Shemitz, all rights reserved. // Permission is hereby granted to freely //use, modify, and // distribute this source code PROVIDED //that all six lines of // this copyright and contact notice are //included without any // changes. Questions? Comments? Offers of work? //mailto:jon@midnightbeach.com // -------------------------------------------- // Добавление в TForm функциональной реализации IUnknown. interface uses Classes, Forms; type TInterfacedForm = class (TForm, IUnknown) private fRefCount: integer; protected function QueryInterface( const IID: TGUID; Obj): Integer; stdcall; function _AddRef: Integer; stdcall; function _Release: Integer; stdcall; public property RefCount: integer read fRefCount write fRefCount; end; implementation uses Windows; // для E_NOINTERFACE // Код IUnknown основан на исходном тексте TInterfacedObject function TInterfacedForm.QueryInterface ( const IID: TGUID; out Obj): Integer; begin if GetInterface(IID, Obj) then Result := 0 else Result := E_NOINTERFACE; end; function TInterfacedForm._AddRef: Integer; begin Inc(fRefCount); Result := fRefCount; end; function TInterfacedForm._Release: Integer; begin Dec(fRefCount); Result := fRefCount; if fRefCount = 0 then Destroy; end; end.Как видите, все очень просто. Оглядываясь назад, я не могу понять, почему мне потребовалось на это так много времени. Наверное, меня сбило с толку предположение о том, что программа, полученная при добавлении интерфейсов к форме, не будет компилироваться из-за своей потенциальной ненадежности. Впрочем, во время своих экспериментов я обнаружил еще одну проблему, связанную с реализацией интерфейсов в Delphi. О ней тоже следует рассказать перед тем, как идти дальше.
Использование данных
Цель любого пользовательского интерфейса — организация эффективного взаимодействия с пользователем. Пользователь должен видеть достаточно данных, чтобы принять и реализовать решение (или по крайней мере понять, что на основании представленных данных это сделать невозможно). В графических деревьях объект удобно выбирать двойным щелчком или клавишей «пробел».
После того как пользователь выберет какой-либо объект, ваше приложение должно идентифицировать его. Текст, отображаемый в элементе, не всегда однозначно определяет объект (он может повторяться в других объектах), поэтому каждый объект обычно снабжается уникальным идентификатором. Такие идентификаторы должны быть короткими, чаще всего— числовыми. Чтобы обеспечить уникальность нового идентификатора, достаточно прибавить 1 к максимальному существующему значению.
Замечание
Хотя мы используем свойство Index для организации иерархии в элементе, это вовсе не означает, что оно остается постоянным для каждого объекта. Свойство Index класса TOutline изменяется при каждом изменении содержимого TOutline; это относительное значение, не связанное с конкретными объектами.
Идентификатор связывается с самим объектом. В дальнейшем по нему можно узнать, какой объект выбрал пользователь. В большинстве элементов, содержащих строковые объекты, также хранятся и связанные со строками объектные указатели. Эта часть интерфейса TString используется многими элементами. Вы можете сохранить указатель на любой объект или просто значение, похожее на указатель. Можно взять положительное целое число (тип данных cardinal), преобразовать его в TObject и сохранить в этом свойстве (обычно оно называется Objects). Если идентификатор не является целочисленным значением, придется создать специальный класс для хранения данных:
type TMyClass = class(TObject) public ID : String; end; begin ... NewIDObject := TMyClass.Create; NewIDObject.ID :=ItemTable.FieldByName ('ID').AsString; MyOutline.AddChildObject(0, ItemTable.FieldByName('Description').AsString, NewIDObject);В компоненте TOutline эти указатели можно получить через Items[Index].Data, вместо того чтобы обращаться к свойству Objects, как это делается в большинстве элементов (и еще одно отклонение от нормы: значения Index начинаются с 1, а не с 0, как в большинстве списков). Указатель связывает объект, порожденный от TObject (то есть экземпляр любого класса), с объектом иерархии. Вам придется определить новый класс для хранения идентификатора, а затем создавать экземпляр этого класса для каждого загружаемого объекта, заносить в него идентификатор и правильно устанавливать указатель.
Чтобы добраться до идентификатора, можно воспользоваться следующим фрагментом кода:
with MyOutline do ThisID := (Items[SelectedItem].Data as TMyIDClass).ID;Возможно, вашему приложению будет недостаточно одного идентификатора и потребуется дополнительная информация. По значению идентификатора можно найти нужную информацию в таблице. Кроме того, можно расширить определение TMyIDClass и сохранять дополнительную информацию в самих объектах.
Помните, что свойство Objects или Data не будет автоматически уничтожать эти объекты. Поэтому либо сделайте их потомками TComponent, чтобы их мог уничтожить владелец компонента, либо переберите элементы списка в деструкторе или обработчике FormDestroy и уничтожьте их самостоятельно. Если вы корректно используете свойство Count, в одном фрагменте кода можно спокойно уничтожать объекты, которые были (или не были) созданы в другом фрагменте.
with MyOutline do for Counter := Count downto 1 do (Items[Counter].Data as TMyIDClass).Free;Обратите внимание на то, что в этом фрагменте уничтожение объектов в порядке «снизу вверх» в цикле for..downto оказывается чуть более эффектив ным, потому что списку при этом не приходится перемещать объекты для заполнения пустых мест.
Использование файлов в памяти
Дневник №16 (1 апреля): Один из самых частых вопросов о Delphi — как написать приложение, существование которого в системе ограничивается одним экземпляром. За последний год я обнаружил несколько решений этой задачи. Одно из них оказалось таким интересным, что я решил описать его здесь.
Чтобы приложение могло обнаружить факт существования другого своего экземпляра, оно должно как-то обратиться c запросом к системным данным. В Windows 3.1 приложение могло узнать о существовании предыдущего экземпляра по значению hPrevInst, однако в Windows 95 все изменилось.
Один из способов заключается в использовании модуля WalkStuf, разработанного мной раньше. Функция ModuleSysInstCount возвращает значение, равное количеству выполняемых копий программы. Приложение может воспользоваться этой функцией и, если возвращаемое значение отлично от нуля, просто завершить работу. К сожалению, этот способ не работает в NT.
Для обмена информацией между приложениями обычно применяется
уникальный глобальный ключ, доступный для всех экземпляров программы. Классический пример — использование уникального файла. При запуске
приложение проверяет, существует ли файл с заданным именем (например, FOOBAR99.DAT). Если такой файл существует, значит, в настоящее время уже работает другой экземпляр программы. Если файл не найден, новый экземпляр программы создает его. Завершая свою работу, программа удаляет файл.
Одна из проблем подобного подхода связана с возможными аномалиями (например, «зависанием» системы или сбоем питания). Поскольку «флаг» (в данном случае — файл) хранится на постоянном носителе, он сохранится и после перезагрузки. В этом случае первый запущенный экземпляр программы «увидит» файл, решит, что в системе уже работает другой экземпляр, и немедленно завершится. В итоге программа вообще перестанет работать. Вам придется наводить порядок, удалять файл и возвращать систему к нормальному состоянию.
Win95 предоставляет более приятную альтернативу — общие файлы в памяти. При этом файл представляет собой временную область памяти (или по крайней мере трактуется как область памяти, даже если он временно выгружается на диск). В отличие от многих ресурсов Win95 файлы в памяти могут совместно использоваться несколькими процессами.
Я создал простейшее приложение для проверки теории о том, что файлы в памяти могут применяться для поиска других экземпляров программы.
На рис. 16.3 изображено рабочее окно приложения, а в листинге 16.3 приведен его исходный текст.
s?ис. 16.3. Программа, запускаемая в единственном экземпляре
Листинг 16.3. Простейшая программа, запускаемая лишь в одном экземпляре
?азумеется, сама форма ничего не делает. Каждый последующий экземпляр программы должен обнаруживать присутствие предыдущего экземпляра и автоматически прекращать работу. И хотя эту ситуацию можно перехватить в стартовом коде формы, намного разумнее делать так, чтобы новый экземпляр вообще не отображался на экране. Следовательно, проверка должна выполняться еще до запуска приложения.
Использование inheritedс переопределенными свойствами
Предположим, вы разрабатываете VCL-компонент Delphi (например, потомок TDrawGrid)и хотите предпринять некоторые особые действия в тот момент, когда пользователь (в нашем случае — программист) изменяет свойство ColCount. Это можно сделать двумя способами; выбор зависит от того, хотите вы получить простое уведомление об изменении или вам необходимо ограничить набор возможных значений ColCount.
Свойство ColCount определяет количество столбцов в сетке. Его значение, как и значение большинства свойств, хранится в private-поле (в нашем случае — FColCount) и изменяется private-методом (SetColCount). Следовательно, когда в программе встречается строка
ColCount := AValue;
или значение ColCount изменяется в инспекторе объектов в режиме конструи рования, вызывается метод SetColCount, который с помощью других private-методов изменяет значение переменной FColCount и вносит необходимые изменения в сетку. Все это инкапсулировано и недоступно для вмешательства извне.
Однако разработчики исходной версии TDrawGrid предусмотрели, что при создании компонентов-потомков может потребоваться уведомление об изменении количества столбцов — поэтому после внесения изменений, но перед их отображением, вызывается метод SizeChanged. Метод SizeChanged является динамическим, то есть его можно переопределить, и после этого при каждом изменении количества столбцов (или строк) будет вызываться новая версия SizeChanged. См. листинг 9.9.
Листинг 9.9. SIZECHAN.SRC
{ Потомок TDrawGrid с переопределенным методом SizeChanged. Это позволяет компоненту-потомку узнавать об изменении количества столбцов или строк. } { В секции interface... } type TMyGrid = class(TDrawGrid) protected procedure SizeChanged(OldColCount, OldRowCount: Longint); override; end; { В секции implementation... } procedure TMyGrid.SizeChanged(OldColCount, OldRowCount: Longint); begin { Выполняем любые необходимые действия } end;Переопределение SizeChanged позволит получать необходимые уведомления, но плохо подходит для контроля за количеством столбцов (скажем, если число столбцов в нашей сетке не должно превышать 3). К моменту вызова SizeChanged (обратите внимание на прошедшее время — Changed — в названии метода) изменения уже внесены. Лучшее, что мы можем сделать, если свойство ColCount стало равно 4, — заменить его на 3 и повторить весь процесс.
Чтобы как можно раньше узнавать об изменениях, мы можем переопределить само свойство ColCount, задав для него новые методы доступа (см. объявление TMyGrid в листинге 9.10). Такое переопределение скрывает свойство ColCount предка. Если теперь в программе встретится строка:
ColCount := AValue;
будет вызван наш, невиртуальный метод SetColCount. Как видно из текста метода (см. листинг 9.10), мы сначала проверяем, не превышает ли новое количество столбцов 3, и если не превышает — вносим изменения.
Листинг 9.10. SETCOLCT.SRC
{ Потомок TDrawGrid, переопределяющий свойство ColCount
с новыми методами доступа. Это позволяет компоненту-потомку
управлять количеством столбцов. }
{ В секции interface... }
type TMyGrid = class(TDrawGrid) private function GetColCount: LongInt; procedure SetColCount(Value: LongInt); published property ColCount: LongInt read GetColCount write SetColCount default 0; end; { В секции implementation... } function TMyGrid.GetColCount: LongInt; begin Result := inherited ColCount; end; procedure TMyGrid.SetColCount(Value: LongInt); begin if Value <= 3 then inherited ColCount := Value; end;Но, вероятно, самое интересное в переопределяемых свойствах — способ их изменения. Мы не можем непосредственно модифицировать значение private-поля FColCount. Впрочем, прямая модификация привела бы к нежелательным эффектам из-за пропуска ряда необходимых действий, сопровожда ющих изменение числа столбцов. Мы не можем вызвать метод SetColCount предка, потому что он определен в разделе private. А попытка вставить в наш метод SelColCount строку вида
ColCount := Value;
приведет к бесконечной рекурсии и переполнению стека.
Правильный ответ заключается в использовании ключевого слова inherited с именем свойства:
inherited ColCount := Value;
Возможность использования inherited с именем свойства предка не так хорошо документирована, как его применение к унаследованным public- и protected-методам. Для кого-то такая возможность станет приятной неожиданностью, но она вполне в духе Object Pascal.
Использование макросов в редакторе Delphi
В редакторе Delphi можно записывать макросы, автоматизирующие ввод повторяющихся фрагментов— но узнать об этом можно разве что случайно; в справочных файлах Delphi это средство не документировано1.
Во время редактирования текста программы можно записать последовательность нажатий клавиш в виде макроса и потом воспроизвести ее. Чтобы начать запись макроса, нажмите Ctrl+Shift+R и введите нужную последовательность клавиш. Запись прекращается повторным нажатием Ctrl+Shift+R. Макрос воспроизводится клавишами Ctrl+Shift+P.
Редактор Delphi — не WinWord и не WordPerfect, и поддержка макросов в нем ограничена: запоминается лишь один набор клавиш. Кроме того, нажатие во время записи макроса любых клавиш, вызывающих переход к другому окну, отменяет процесс записи. Например, если последняя операция Find представляла собой простой поиск, то при нажатии F3 диалоговое окно не выводится (при успешном поиске) и клавиша F3 включается в макрос. Но если ранее выполнялся поиск с заменой, F3 выведет диалоговое окно с запросом подтверждения, и запись макроса прервется.
Даже при таких ограничениях макросы могут принести немалую пользу — вы можете определять закладки и переходить к ним, выполнять поиск с изменением критерия, копировать и вставлять фрагменты текста.
Например, после ввода заголовка метода в объявлении класса мне часто приходится копировать этот заголовок в секцию implementation модуля, вставлять перед ним имя класса с точкой и вводить пару begin..end. Если тщательно продумать последовательность операций, все эти действия можно записать в одном универсальном макросе. В листинге 9.16 приведен возможный набор клавиш, которые выполняют эту задачу при условии, что текстовый курсор находится в строке с заголовком метода.
Кстати, в моем примере использованы стандартные (Default) настройки клавиатурных комбинаций редактора. Если у вас установлен другой режим, возможно, макрос придется изменить.
Листинг 9.16. HEADING.TXT
{ Ниже приведена последовательность нажатий клавиш для вставки заголовка
метода в секцию implementation модуля и добавления пары begin..end.
Управляющие сочетания клавиш заключены в фигурные скобки.
После двойного символа "косая черта" следует комментарий.
Предполагается, что модуль заканчивается ключевым словом "end."}
{Ctrl+Shift+R} // Начало записи {HOME} // Перейти к началу строки {Shift+DOWN} // Выделить строку {Ctrl+C} // Скопировать выделенную строку {Ctrl+END} // Перейти в конец модуля {Ctrl+LEFT} // Перейти в позицию слева от "end." {Ctrl+V} // Вставить скопированную строку {UP} // Перейти к началу вставленной строки {Ctrl+T} // Удалить отступ {Ctrl+RIGHT} // Перейти к имени метода TMyClass. // Ввести имя класса с точкой {END} // Перейти к концу строки {ENTER} // Вставить новую строку begin // Ввести "begin" {ENTER}{ENTER} // Вставить две новые строки после "begin" end; // Ввести "end;" {ENTER} // Вставить новую строку после метода {UP}{UP} // Вернуться к телу метода {RIGHT}{RIGHT} // Создать отступ в два пробела // и приготовиться к вводу {Ctrl+Shift+R} // Остановить записьИспользование RDTSC для измерения временных интервалов на Pentium
В доисторическую эпоху написание быстрых программ не сводилось к правильному выбору алгоритма; программисту приходилось помнить временные характеристики различных команд и измерять время выполнения различных вариантов. Поскольку системный таймер «тикает» лишь каждые 55миллисекунд, при измерениях приходилось повторять одни и те же вычисления сотни тысяч раз или же пускаться на хакерские ухищрения вроде чтения внутренних регистров таймера, чтобы получить значение времени с точностью до 838 наносекунд.
В наши дни появились хорошие компиляторы и быстрые процессоры, в результате чего стало довольно трудно написать какой-нибудь «предельно тупой» код, существенно замедляющий работу программы. Однако по иронии судьбы средство для измерения временных интервалов появилось лишь в процессоре Pentium. Команда RDTSC (Read Time Stamp Counter) возвращает количество тактов, прошедших с момента подачи напряжения или сброса процессора. Где была эта команда, когда мы действительно нуждались в ней?
И все же лучше поздно, чем никогда. Команда RDTSC состоит из двух байтов: $0F 31. Она возвращает в регистрах EDX:EAX 64-битное значение счетчика. Поскольку сопроцессорный тип данных comp представляет собой 64-битное целое, мы можем прочитать текущее значение с помощью кода Delphi, приведенного в листинге 9.3.
Листинг 9.3. RDTSC.SRC
const D32 = $66; function RDTSC: comp; var TimeStamp: record case byte of 1: (Whole: comp); 2: (Lo, Hi: LongInt); end; begin asm db $0F; db $31; // BASM не поддерживает команду RDTSC {$ifdef Cpu386} mov [TimeStamp.Lo],eax // младшее двойное слово mov [TimeStamp.Hi],edx // старшее двойное слово {$else} db D32 mov word ptr TimeStamp.Lo,AX {mov [TimeStamp.Lo],eax - младшее двойное слово} db D32 mov word ptr TimeStamp.Hi,DX {mov [TimeStamp.Hi],edx - старшее двойное слово} {$endif} end; Result := TimeStamp.Whole; end;Одна из проблем, с которой вы столкнетесь при использовании команды RDTSC, заключается в том, что функции IntToStr и Format('%d') могут работать только со значениями типа LongInt, а не comp. Если этим функциям передается значение типа comp, оно не может превышать High(LongInt), то есть 2147483647. Возможно, эти цифры производят впечатление, если они определяют сумму в долларах, но на Pentium с тактовой частотой 133 МГц это соответствует всего лишь 16 секундам. Если вам потребуется сравнить время работы двух длительных процессов, разность между показаниями таймера в начале и конце работы легко может превысить High(LongInt).
Проблема решается просто. Хотя тип comp соответствует 64-битному целому, на самом деле это тип данных сопроцессора 80х87. Чтобы отформатировать comp функцией Format(), необходимо воспользоваться форматами с плавающей точкой. Функция CompToStr в листинге 9.4 скрывает все хлопотные подробности, причем с ней сгенерированный компилятором объектный код получается более компактным, нежели при непосредственном использовании нескольких вызовов Format().
Листинг 9.4. COMP2STR.SRC
function CompToStr(N: comp): string; begin Result := Format('%.0n', [N]); end;Напоследок скажу лишь следующее. Потребность в измерении временных интервалов сейчас возникает намного реже, чем в былые времена. В то же время с появлением команды RDTSC такое измерение становится удобным и надежным.
На этом замечании я передаю повествование своему соавтору, Эду Джордану. Продолжай, Эд!
Использование шаблона Filter
Если вам захочется поместить фильтр в хранилище, создайте новый подкаталог в каталоге ObjRepos и сохраните в нем файлы FILTER.DPR, FILTMAIN.PAS, CMDLINE.PAS и FILEIO.PAS. Затем выполните команду Projectд Add to Repository и введите необходимую информацию. Когда вам в следующий раз придется писать фильтр, вся скучная работа уже будет сделана заранее - возьмите шаблон, подправьте параметры и перепрограммируйте рабочий цикл.
Использование сохраненных процедур
Сохраненные процедуры (stored procedures) напоминают SQL с добавлением условных операторов и циклов. На языке InterBase можно написать так называемые процедуры выборки (select procedures), которые аналогично SQL-запросам возвращают некоторое количество записей из набора. С помощью процедурного языка можно перебрать записи, входящие в набор (полученный с помощью запроса или другой, вложенной процедуры выборки), и выполнить с ними необходимые действия. Пример, написанный на командном языке InterBase, приведен в листинге13.7 (нумерация строк используется в последующих комментариях).
Листинг 13.7. Процедуры выборки в InterBase
1. CREATE PROCEDURE GETCHILDREN (STARTING_ITEM_ID SMALLINT, THISLEVEL SMALLINT) 2. RETURNS(ITEM_ID SMALLINT, DESCRIPTION CHAR(30), ITEMLEVEL SMALLINT) AS 3. BEGIN 4. FOR 5. SELECT T1.ITEM_IDM T1.DESCRIPTION 6. FROM ITEMS T1 7. WHERE T1.PARENT_ID = :STARTING_ITEM_ID 8. INTO :ITEM_ID, :DESCRIPTION 9. DO BEGIN 10. ITEMLEVEL = THISLEVEL + 1; 11. SUSPEND; 12. FOR 13. SELECT T1.ITEM_ID, T1.DESCRIPTION, T1.ITEMLEVEL 14. FROM GETCHILDREN(:ITEM_ID, :ITEMLEVEL) T1 15. INTO :ITEM_ID, :DESCRIPTION, :ITEMLEVEL 16. DO BEGIN 17. SUSPEND; 18. END 19. END 20. END;Подобные итерации идеально подходят для просмотра иерархических данных в обоих направлениях, потому что сохраненные процедуры рекурсивны. Такую процедуру можно вызывать из нее самой, чтобы определить детей текущего объекта, затем получить их детей и т. д. Вместо того чтобы сразу получить все записи одного поколения и переходить к следующему, при этой стратегии мы сначала определяем первого потомка объекта, затем — его первого потомка (т. е. первого внука исходного объекта) и т. д. до нахождения последнего потомка.
На языке InterBase SUSPEND означает, что возвращаемая по RETURNS информация должна заноситься в результирующий набор в виде очередной записи. Первый оператор SUSPEND (строка 11) возвращает значения из первой записи запроса, определяющего непосредственных потомков STARTING_ITEM_ID (строки 5_8). Следующий SUSPEND (строка 17) возвращает результат рекурсив ного вызова процедуры выбора GETCHILDREN. До тех пор пока этот второй вызов находит записи (то есть до тех пор, пока у объекта находятся потомки), второй SUSPEND возвращает их исходной вызывающей процедуре. Когда объекты кончаются, вызывающий код продолжает свою работу и с помощью первого SUSPEND возвращает вторую запись исходного запроса. Если не сбросить переменную ITEMLEVEL во внешнем цикле (строка 10), в ней будет храниться значение из последней итерации внутреннего цикла (строка 15).
Для вызова процедур выборки InterBase следует пользоваться компонентом TQuery, а не TStoredProc. Синтаксис выглядит просто:
with Query1 do begin SQL.Clear; SQL.Add('SELECT * FROM GetChildren(' + IntToStr(CurrentItemID) + ',0)'); Open; end;Полученный набор будет содержать всех потомков текущего объекта с указанием их уровня.
Использование SQL
Если ваша иерархия слишком велика и ее не удается полностью загрузить в память, подумайте о решении, основанном на SQL. Если количество уровней рекурсии известно заранее, для установления связи между «поколениями» можно воспользоваться вложенными запросами SQL, как показано в листинге 13.6:
Листинг13.6. Использование SQL для просмотра трех поколений иерархии
SELECT * FROM Items T1 WHERE T1.Parent_ID IN (SELECT T2.Item_ID FROM Items T2 WHERE T2.Parent_ID IN (SELECT T3.Item_ID FROM Items T3 WHERE T3.Parent_ID = 'Fred'))В этом SQL-запросе участвуют ровно три поколения; возвращаются только те записи, которые являются «правнуками» записи Fred. Чтобы получить, например, детей и внуков одновременно, придется выполнить два запроса, а затем воспользоваться SQL-конструкцией UNION или объединить результаты с помощью INSERT INTO или временных таблиц.
Чтобы отыскать родителя объекта, найдите запись, у которой Item_ID совпадает с Parent_ID текущего объекта. Чтобы отыскать всех детей объекта, необходимо найти все записи, у которых Parent_ID совпадает с Item_ID текущего объекта. Чтобы отыскать всех родственников, найдите все объекты с тем же значением Parent_ID (обратите внимание: исходный объект также войдет в этот набор, если специально не исключить его). Чтобы определить всех потомков объекта, следует найти всех его детей, затем перебрать объекты полученного набора и определить их детей, затем перебрать объекты следующего полученного набора и т. д.
Использование свойства Tag
Наверное, вас давно интересует вопрос— как RESOLVER32 определяет, какое из введенных значений необходимо обработать? Все очень просто: у каждого элемента есть свойство Tag, по нему можно выделить текстовое поле, которое получает строку для преобразования. Свойствам Tag текстовых полей назначаются целые числа, начиная с 1 для текстового поля edIPName и заканчивая 6 для edProtoNo. Затем обработчики событий OnClick этих текстовых полей используются для изменения свойства Tag формы. Следующий фрагмент показывает, как это делается, на примере текстового поля edIPName1:
procedure TfrmMain.edIPNameClick(Sender: TObject);
begin
frmMain.tag := edIpName.tag;
end;
При нажатии кнопки Resolve RESOLVER32 анализирует frmMain.tag в операторе case и присваивает значение нужному свойству. В листинге 5.13 показано, как это делается.
Листинг 5.13. Использование свойства tag для определения того, какое из введенных значений следует преобразовать
procedure TfrmMain.btnResolveClick(Sender: TObject); begin btnResolve.Enabled := FALSE; Screen.Cursor := crHourGlass; if CsSocket1.Access = NonBlocking then btnAbortRes.Enabled := TRUE; pnStatus.Color := clBtnFace; pnStatus.UpDate; case tag of begin edHostName.Text := ''; edHostName.Update; pnStatus.Caption := Concat('Resolving ',edIPName.Text); pnStatus.UpDate; CsSocket1.HostName := edIPName.Text; end; begin edIPName.Text := ''; edIPName.UpDate; pnStatus.Caption := Concat('Resolving ',edHostName.Text); pnStatus.UpDate; CsSocket1.HostName := edHostName.Text end; begin edPortName.Text := ''; edPortName.UpDate; pnStatus.Caption := Concat('Resolving ', edServiceName.Text); pnStatus.UpDate; CsSocket1.WSService := edServiceName.Text end; begin edServiceName.Text := ''; edServiceName.UpDate; pnStatus.Caption := Concat('Resolving ', edServiceName.Text); pnStatus.UpDate; CsSocket1.WSPort := edPortName.Text end; begin edProtoNo.Text := ''; edProtoNo.UpDate; pnStatus.Caption := 'Resolving protocol name.'; pnStatus.UpDate; CsSocket1.WSProtoName := edProtoName.Text; end; begin edProtoName.Text := ''; edProtoName.UpDate; pnStatus.Caption := 'Resolving protocol number.'; pnStatus.UpDate; CsSocket1.WSProtoNo := edProtoNo.Text; end; end; end;Использование TQuery для определения набора подчиненных записей
С помощью TQuery можно определить набор подчиненных записей, для этого базовый набор данных (TTable или TQuery) передает свои значения свойству SQL в качестве параметров динамического запроса. В приведенном выше примере свойство SQL подчиненного объекта TQuery выглядит примерно так:
SELECT * FROM Employee T1 WHERE T1.Boss_ID = :Emp_IDСвойство TQuery.DataSource показывает, откуда берется значение параметра. В приведенном выше SQL-запросе значение Emp_ID берется из TQuery.DataSource. DataSet.FieldByName('Emp_ID') (имя параметра должно совпадать с именем поля источника). При каждом изменении базового поля запрос выполняется заново с новым значением параметра.
Подчиненные TQuery используют динамический SQL-запрос вместе со свойством DataSource. Запрос называется динамическим, потому что он использует параметр вместо того, чтобы заново строить весь SQL-текст запроса при каждом изменении критерия. Однако, если критерий запроса может иметь различную структуру, вам придется воссоздавать весь SQL-оператор в текстовом виде; в этом случае параметры не помогут.
Если вы захотите в большей степени контролировать процесс отображения записей или пожелаете передать измененный SQL-запрос через TQuery (не позволяя свойству TQuery.DataSource сделать это за вас), можно воспользоваться программным кодом вместо задания свойства MasterSource. Например, можно добавить некоторые записи к тем, которые были отобраны в соответствии с критерием. Желательно делать это в тот момент, когда обработчик OnDataChanged подключается к базовому TDataSource (см. листинг13.2).
Листинг 13.2. Добавление записей, не удовлетворяющих основному критерию
procedure TForm1.DataSource1DataChange (Sender : TObject; Field : TField); begin if (Field = nil) or (Field.FieldName = 'Emp_ID') then begin Query2.DisableControls; Query2.Close; with Query2.SQL do begin Clear; Add('SELECT *'); Add('FROM employees T1'); Add('WHERE T1.Boss_ID = ' + Table1.FieldByName('Emp_ID').AsString); Add('OR T1.Boss_ID IS NULL'); { // дополнительный код } end; Query2.Open; Query2.EnableControls; end; end;При этом будут извлечены записи, принадлежащие текущему Boss_ID, а также те, которые не принадлежат никакому Boss_ID (работники, у которых вообще нет начальника).