Версия для печати


Простые CGI приложения на Дельфи. Просто рисуем свой счетчик
http://www.delphikingdom.com/asp/viewitem.asp?catalogID=309

Елена Филиппова
дата публикации 16-10-2000 00:00

Простые CGI приложения на Дельфи. Просто рисуем свой счетчик

В продолжение статьи Алексеея Еремеева "Простые CGI-приложения на Delphi" я попробую рассказать о генерации изображений "на лету" на примере счетчика посещений.

Материал этот предназначен для начинающих программистов, тем не менее предполагается, что перед его прочтением вы познакомились со статьей Алексея Еремеева "Простые cgi-приложения на Delphi" и материалом Евгения Акованцева "К вопросу о построении гостевой книги" из раздела "Hello, Word!". А так же пытались разобраться с методами и свойствами класса TWebModule.

Генерация изображения "на лету"

Смысл этой красивой фразы довольно прост - требуется вывести на HTML-странице картинку, которой нет. То есть просто нарисовать ее перед тем, как сервер сгенерит страницу по запросу клиента.
Области применения этой технологии - вывод некой динамической информации в виде графического изображения. Например, счетчик посещений сайта, всевозможные статистические графики, рейтинги и наградные(именные) маркеры.
Начнем с самого простого - счетчика посещений.
Практически каждый сайт, посвященный web-программированию, содержит пример такого счетчика. Алгоритм его создания довольно прост:

Можно было бы не повторяться, но некоторые моменты в самых распространенных в сети примерах, мягко говоря не совсем корректны.

Тех, кто крайне отрицательно относится к лирическим отступлениям, просьба следующий раздел не читать, дабы не раздражаться лишний раз.

Где хранить значение счетчика?

Вопрос, который не имеет никакого отношения к генерации изображений, но ответить на который надо, пржде чем писать такой скрипт.
Чаще всего предлагается использовать для хранения текущего значения счетчика таблицу в базе данных. Заводите табличку с соответствующим полем и апдейтите его при каждой загрузке скрипта. Дешево и сердито.
Тут даже не важно, есть ли у вас возможность использовать БД или нет, здесь важен сам подход. Базу данных надо использовать тогда, когда есть необходимость в выборках по различным условиям, поиске и сортировке. Что вряд ли требуется для функционирования счетчика посещений, даже с минимальной статистикой. Не все то, что на первый взгляд "проще" по реализации, на самом деле лучшее решение.
В полном соответствии с принципом "бритвы Оккама".

В 13-ом веке в Англии жил философ Уильям Оккам (ударение на первый слог). Занятия его философией и теологией для большей части программистов покрыты мраком, но имя его известно, по крайней мере для любителей фантастики, благодаря знаменитому принципу "бритвы Оккама". Принцип этот гласит - "Отсекай все лишнее" или "Не вводите новых сущностей без особой надобности".

Стоит ли вводить новую сущность (БД) между клиентом и сервером в этом случае? Ведь чем длиннее цепочка, тем больше шансов у нее порваться :о)

Послушаемся мудрого Оккама, не будем тревожить БД по мелочам и будем хранить текущее значение счетчика в файле.

Пишем и читаем

Важно ! Для того, чтобы писать в файл на диске на Вашем Web-сервере, необходимо, чтобы соответствующий каталог имел атрибут Change.

В нашем примере функция ReadCounter возвращает текущее значение счетчика, читая его из файла. При этом, если файла нет на диске, она его создает и устанавливает значение счетчика в 1. Такми образом нет необходимости специально создавать файл на диске с нужным именем и начальным значением. Функция все сделает сама.
Формат файла может быть текстовый или бинарный. Разницы нет никакой, кроме возможности (или невозможности) поправлять значение счетчика "ручками".
Я буду использовать бинарный файл. Если Вам захочется переделать код на работу с текстовым файлом, смотрите Help, функция AssignFile и далее по ссылкам.

Function ReadCounter : String;
Var   FHandle     ,
      Counter     : Cardinal;
      FileName    : String;
Begin
    Result:= '0';
    // имя файла со значением счетчика
    FileName:='counter.dat';

    Counter:=0;

    Try
	// пытаемся открыть или создать этот файл и читать из него	
      IF NOT FileExists(FileName)
      Then FHandle:=FileCreate(FileName)
      Else Begin
             FHandle:=FileOpen(FileName,fmOpenReadWrite);
             FileRead(FHandle,Counter,SizeOF(Counter));
           End;

      Result:= IntToStr(Counter);
	// увеличиваем счетчик 		  
      Inc(Counter);

	// и пытаемся записать новое значение обратно в файл 		  	  
      FileSeek(FHandle,0,0);
      FileWrite(FHandle,Counter,SizeOF(Counter));
      FileClose(FHandle);
    Except
    End;
   

End;

Функция фозвращает сразу символьное значение счетчика, так удобнее для ее дальнейшего использования. Тип Cardinal это беззнаковое 32-битное целое число, его диапазон - < 0 .. 4 294 967 295 > , так что любителям накручивать счетчики будет где развернуться :о)

GIF или JPEG ?

Выводить картинку можно в одном из этих форматов, но в каком лучше? С точки зрения программирования разницы нет никакой, ну кроме того, что будут использоваться разные объекты. Предпочтение конкретному формату зависит от Ваших личных вкусов и типа изображения. Дело в том, что формат GIF лучше подходит для четких изображений (надпись, график и т.п.), а формат JPEG - для картинок с плавными переходами цветов.
Так что , можете поэкспериментировать и решить, что больше нравится именно Вам.
В нашем примере будем рисовать GIF, для этого нам необходим соответствующий класс, который поддерживает формат GIF.
В сети есть немало свободно распространяемых графических компонент, я буду использовать в своем примере RXGIF. Не потому, что он лучше для этого подходит (скорее всего наоборот, он тяжеловат, так как реализует много возможностей, которые нам даже и не пригодятся ) , но у меня он уже есть и искать специально что-то другое только для этого примера нет надобности.

Рисуем счетчик

Итак, приступим. Создаем новое приложение:
File-> New -> Web Server Application -> CGI Stand Alone Executable.
В WebModule в Actions добавляем новое действие (Add Item).
Устанавливаем для него Default:=True. В обработчике этого действия (WebActionItem) и пишем нужный нам код.
Смысл этих действий более подробно описан в материале "К вопросу о создании гостевой книги".

Итак, алгоритм следующий:
  1. Читаем из файла значение счетчика;
  2. Создаем объект TBitmap и пишем, то есть рисуем на его канве текст;
  3. Создаем объект TGIFImage и передаем ему изображение, то есть конвертируем в формат GIF сформированную картинку. Это действие необходимо, так как сразу рисовать в формате GIF мы не можем;
  4. Создаем объект TMemoryStream и "переливаем" в бинарный поток наше изображение;
  5. Передаем этот поток в Response и отправляем его в броузер клиента.
  6. Ну и не забываем почистить за собой.
procedure TWebModule1.WebModule1WebActionItem1Action(Sender: TObject;
  Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
Var
   Image      : TGIFImage;
   Stream     : TMemoryStream;
   BMP        : TBitMap;
begin
   Image:=TGIFImage.Create;
   BMP:=TBitMap.Create;
   Stream:=TMemoryStream.Create;

   Try 	   
   	BMP.Width:=100;
   	BMP.Height:=20;

   	BMP.Canvas.TextOut(0,0, ReadCounter);

   	Image.Assign(BMP);
   	Image.SaveToStream(Stream);
   	Stream.Position:=0;
	Try
   		Response.ContentType:='image/gif';

   		Response.ContentStream:=Stream;
   		Response.SendResponse;
   	Except
		Stream.Free;
   	End;
   Finally
   	Image.Free;
   	BMP.Free;   
   End; 

end;

Теперь при каждом обращении к адресу http://localhost/cgi-bin/counter.exe , в уголке броузера будет выводится новое значение счетчика. Можно вcтавлять этот вызов в свои html-страницы.

Примечание:

Кого-то может заинтересовать , отчего объекты Image и BMP я освобождаю в любом случае, а Stream только в случае неудачного завершения персылки данных клиенту?
Дело в том, что сразу после успешной отработки метода Response.SendResponse объект Stream окажется ... уже разрушенным.
И попытка освободить его еще раз приведёт к Access Violation. Можете поэкспериментировать сами.

Передача параметров

Цель примера - показать в общем эту технологию, но оставлять этот код как образец счетчика нельзя, больно он убого выглядит.
Стоило возиться с графикой, чтобы просто "накарябать цифирьку"?..
Усложним нашу задачу. Сделаем более универсальный счетчик с настраиваемыми параметрами.
Прежде чем наводить красоту, нужно решить, как мы будем задавать всякие разные параметры.
Внимательно прочитайте Help по теме TWebRequest, нас будет интересовать свойство QueryFields : TStrings. Это список параметров запроса по методу GET, то есть тех параметров, которые передаются в теле строки URL.
Например, запрос
http://localhost/cgi-bin/counter.exe?par1=10&par2=Test& 
передаст на сервер список из двух строк: 'par1=10' и 'par2=Test'

Доступ к списку имен параметров и их значений предоставляют свойства класса TStrings :
property Names[Index: Integer]: string; 
и
property Values[const Name: string]: string; 
соответственно.

И если нас интересует входной параметр Par, его значение можно получить из Request.QueryFields.Values['Par'], если такого параметра в строке запроса не было, в качестве его значения получим пустую строку .
Данные, передаваемые клиенту методом POST (поля формы), будут доступны через TWebRequest.ContentFields.

Для того, чтобы получать нужные нам значения независимо от способа передачи параметров, напишем вот такую функцию:


//------------------------------------------------------------
//      Получить входящие данные независимо от метода запроса
//
//      Порядок поиска : 1) Из входящего потока (GET )
//                       2) Из полей формы      (POST)
//                       3) Из cookies
//------------------------------------------------------------
Function GetRequest( Request : TWebRequest ; Value : String) : String;
Begin
    Result:=Request.QueryFields.Values[Value];

    Result:=IsEmpty( Result , Request.ContentFields.Values[Value]);
    Result:=IsEmpty( Result , Request.CookieFields.Values[Value]);

    Result:=Trim(Result);
End;
//------------------------------------------------------------
Function IsEmpty( Value , Empty : String): String;
Begin
    IF Trim(Value) = '' Then Value:=Empty;
    Result:=Value;
End;
//------------------------------------------------------------
Для счетчика это не так важно, но в дальнейшем такая функция пригодится.

Красота - это страшная сила!

А теперь можно и красоту наводить. Что бы такого придумать полезного?
Во-первых, заставим скрипт читать значение счетчика из разных файлов. Например будем передавать ему в качестве параметра номер счетчика. Тогда можно будет использовать один и тот же скрипт на разных страницах, передавая ему разные номера (параметр Data) .
Во-вторых, введем такое понятие, как разрядность счетчика, вместо "162" будем выводить "000162", так выглядит интереснее. И саму разрядность будем передавать по параметру (параметр NoCount).
И, наконец, просто раскрасим его, читая из входного потока и цвет (параметр Color).

Для защищенной работы скрипта необходимо предусмотреть систему умолчаний, на случай если параметры будут не заданы или неверно заданы.
Например вот так:
   Text:=GetRequest(Request,'Color');
   IF NOT IdentToColor( Text , Color )  Then Color:=clWhite;

   No:=GetNumeric(GetRequest(Request , 'NoCount');
   IF No = 0 Then No:=10;
Здесь использована функция GetNumeric, которая возвращает целое число вместо строки символов. Ее реализацию смотрите в коде примера.

Вот так изменится код формирования изображения:
// Устанавливаем параметры шрифта, в том числе полученный цвет 
BMP.Canvas.Font.Color:=TColor(Color);
BMP.Canvas.Font.Size:=12;
BMP.Canvas.Font.Name:='Arial';
BMP.Canvas.Font.Style:=BMP.Canvas.Font.Style + [fsBold];

// Рассчитываем размер картинки исходя из выбранного шрифта
// ну и прибавляем по пикселю со всех сторон, для красоты :о)  
CharW:=BMP.Canvas.TextWidth('0');
BMP.Width:=No*CharW + 2;
BMP.Height:=BMP.Canvas.TextHeight('0') + 2;

// Готовим фон счетчика и рисуем его!
BMP.Canvas.Brush.Color:=ClBlack;
BMP.Canvas.FillRect(RECT(0,0 , BMP.Width,BMP.Height));
BMP.Canvas.TextOut(1 ,1 , ReadCounter(No , FileName));

Вот так будет выглядеть наш счетчик, если никаких параметов не задавать вообще.
http://localhost/cgi-bin/counter.exe

А вот так с параметрами.
http://localhost/cgi-bin/counter.exe?NoCount=5&Color=clAqua

Красота это страшная сила...

Итого:

Естественно, что после таких наших нововведений и функция ReadCounter и обработчик события OnAction изменились. Приводить их здесь полностью нет смысла, скачайте пример и поэкспериментируйте с кодом. Это будет удобнее и полезнее.

Вы еще не обратили внимание на размер исполняемого кода нашего скрипта?
Ничего не поделаешь.. за удобство надо платить.

Если переписать этот счетчик в стиле Алексея Еремеева, размер исполняемого кода сократится в два раза :о)

Елена Филиппова
Специально для Королевства "Delphi"



К материалу прилагаются файлы: