Алгоритмы
Для начала решим, как мы будем действовать. Когда-то давно я искал хорошую реализацию градиентной заливки, но у них у всех был большой недостаток - громоздкость и нечитабельность алгоритма. Кроме того, было только два вида - горизонтальная и вертикальная заливка. В моей статье вид заливки ограничится лишь вашей фантазией. Я создал библиотеку градиентных функций и забыл об этой проблеме. Позже я приобрел библиотеку RX и увидел там почти аналогичную реализацию, но опять таки только 2-3 вида заливки. ДАЕШЬ ТВОРЧЕСКУЮ РЕАЛИЗАЦИЮ!
Итак, начнем с того, что чтобы не быть зависимым от вида заливки, нужно цвета держать в массиве. Единожды заполнив массив плавным переходом цветов, его можно использовать для разных видов заливки. Кроме того, используя массив гораздо легче будет сделать множественную заливку - скажем, от синего к красному, потом от красного к зеленому и от зеленого к синему. Давайте рассмотрим алгоритм заполнения массива.
Сразу оговоримся о типе TColorArray.
type TColorArray = array of TColor;
procedure SimpleFillArray(FromColor, ToColor: TColor; var ColorArray: TColorArray; ArrayWidth: Integer); var i: Integer; R1,G1,B1, R2,G2,B2: Byte; begin R1 := GetRValue(ColorToRGB(FromColor)); G1 := GetGValue(ColorToRGB(FromColor)); B1 := GetBValue(ColorToRGB(FromColor)); R2 := GetRValue(ColorToRGB(ToColor)); G2 := GetGValue(ColorToRGB(ToColor)); B2 := GetBValue(ColorToRGB(ToColor));
for i := 0 to ArrayWidth do ColorArray[i] := RGB(R1 - i*(R1 - R2) div ArrayWidth, G1 - i*(G1 - G2) div ArrayWidth, B1 - i*(B1 - B2) div ArrayWidth); end;
Объясним все по порядку. Для начала, нам нужно извлечь отдельные RGB-координаты из цветов FromColor, ToColor. Делается это с помощью функций GetXValue(X=R,G,B). Однако, это не единственный способ получения RGB-координат. Не забудем, что цвет - это обычное целочисленное число. Поэтому, координаты можно достать и так:
R := Color mod $100; G := Color div $100 mod $100; B := Color div $10000;
и так:
R := Color and $FF; G := (Color and $FF00) shr 8; B := (Color and $FF0000) shr 16;
и так:
R := Lo(Color); G := Lo(Color shr 8); B := Lo((Color shr 8) shr 8);
Что вы выберете - ваше дело. Мне удобнее через GetXValue.
Итак, координаты извлечены, затем, согласно алгоритму, заполняются ячейки массива. (х - расстояние от начала массива, в цикле это счетчик i).
В этой процедуре мы заполняем массив простым переходом цветов. Но можно сделать и круче - переход с несколькими цветами, заданными массивом:
procedure ComplexFillArray(Colors: array of TColor; var ColorArray: TColorArray; ArrayWidth: Integer); var ColArray: TColorArray; i,j,Temp: Integer; Equal: Boolean; begin //Вначале проверим число цветов //Если массив пуст: if High(Colors) < 0 then begin raise Exception.Create('Specify at least one color!'); Exit; end;
//Если только один элемент, то //просто заполняем массив этим цветом: if High(Colors) = 0 then begin for i := 0 to ArrayWidth do ColorArray[i] := Colors[0]; Exit; end;
//ширина одной полосы, необходимой для перехода от //одного цвета массива к другому. Естественно, ширина //кратна числу цветов в массиве. Temp := ArrayWidth div (High(Colors)); SetLength(ColArray, Temp + 1);
Equal := (ArrayWidth mod Temp)=0; //булевая переменная //- наличие остатка после деления - сигнализирует о том, //укладываются ли полосы в массив полностью, или нет
for i := 0 to High(Colors) - 1 do begin SimpleFillArray(Colors[i], Colors[i + 1], ColArray, Temp); for j := 0 to Temp do ColorArray[j + i*Temp] := ColArray[j]; end; //если имеет место неполное заполнение, то делаем следующее: //отступаем от конца на расстояние ArrayWidth //mod Temp и закрашиваем от //цвета на этом расстоянии до последнего цвета (см. рисунок )
Теперь мы можем заполнять массив несколькими цветами. Теперь что касается входного параметра ArrayWidth (длина массива). Как определить, какая длина массива нам нужна? Давайте посмотрим на примере функции горизонтальной заливки. Посмотрим, сколько нам нужно в этом случае. Для горизонтальной заливки длина массива соответствует количеству пикселей, размещенных по высоте заливаемой области:
function HorizontalArrayWidth(FillRect: TRect): Integer; begin Result := abs(FillRect.Bottom - FillRect.Top); end;
Теперь, зная длину, можно и заливать. Мой принцип - лучше медленно в начале, но быстро потом, чем наоборот. Всегда рисуйте на временном битмапе, а потом отображайте этот битмап на канву. Тем более это касается сложных нелинейных видов заливки (рассмотрим позже).
procedure HorizontalGradient(Canvas: TCanvas; FillRect: TRect; Colors: TColorArray); var i: Integer; TempBmp: TBitmap; begin TempBmp := TBitmap.Create; TempBmp.Width := abs(FillRect.Right - FillRect.Left); TempBmp.Height := abs(FillRect.Bottom - FillRect.Top);
try With TempBmp do for i := 0 to TempBmp.Height do begin Canvas.Pen.Color := Colors[i]; Canvas.MoveTo( - 1, i); Canvas.LineTo(TempBmp.Width + 1, i); end; Canvas.StretchDraw(FillRect, TempBmp); finally TempBmp.Free; end; end;
Пример использования:
procedure TForm1.HorizontalClick(Sender: TObject); var ColArr:TColorArray; begin SetLength(ColArr, HorizontalArrayWidth(BMP.Canvas.ClipRect) + 1); // не забудем, что индексация // идет от нуля ComplexFillArray([clBlack,clRed, $004080FF, clYellow,clGreen,clBlue, clNavy, clPurple, clBlack], ColArr, HorizontalArrayWidth(BMP.Canvas.ClipRect)); HorizontalGradient(BMP.Canvas, BMP.Canvas.ClipRect, ColArr); Canvas.StretchDraw(Clientrect,BMP); Finalize(ColArr); end;
Я разделил процесс создания - заполнения массива цветами с процессом градиентной заливки потому, что этот массив может повторно использоваться, но в принципе, процесс создания - заполнения можно занести внутрь процедуры заливки в случае единичного использования массива.
По поводу объекта BMP - это глобальный битмап, который я создаю в FormCreate, чтобы не создавать каждый раз временный битмап и форма не мерцала при каждой отрисовке. Полностью демо можно будет скачать.
Аналогично выглядит функция вертикальной заливки. Длина массива соответствует ширине заливаемой области:
function VerticalArrayWidth(FillRect: TRect): Integer; begin Result := abs(FillRect.Right - FillRect.Left); end;
procedure VerticalGradient(Canvas: TCanvas; FillRect: TRect; Colors: TColorArray); var i: Integer; TempBmp: TBitmap; begin TempBmp := TBitmap.Create; TempBmp.Width := abs(FillRect.Right - FillRect.Left); TempBmp.Height := abs(FillRect.Bottom - FillRect.Top);
try With TempBmp do for i := 0 to TempBmp.Width do begin Canvas.Pen.Color := Colors[i]; Canvas.MoveTo(i, - 1); Canvas.LineTo(i, TempBmp.Height); end; Canvas.StretchDraw(FillRect, TempBmp); finally TempBmp.Free; end; end;
До этого, мы рассматривали лишь простые варианты заливки. Теперь перейдем, собственно, к творчеству. Давайте посмотрим, как можно сделать что-нибудь другое. Например, диагональную заливку с левого верхнего к правому нижнему углу. Все, что нужно сделать - это заполнить массив и рисовать линии по диагонали. Длина массива должна быть равна сумме высоты и ширины заполняемой области. Почему? Давайте посмотрим. Процедура заполнения должна выполниться в два приема - вначале закрашиваем левый верхний треугольник, то есть спускаемся по левой стороне области, продолжая линии до верхней стороны области. По достижении нижнего левого угла направление закрашивания меняется. Теперь идем по нижней стороне, продолжая линии до правой стороны (при условии квадратной области), если же область неквадратная, то часть линий будет касаться верхней стороны. Проблема с неквадратностью может быть решена, если создать временный квадратный битмап, стороны которого равны максимальной стороне прямоугольной области. Затем этот битмап закрасить и растянуть на закрашиваемую область с помощью метода StretchDraw (или стандартной функции из модуля Windows - StretchBlt). Аналогично будет проходить закрашивание по диагонали из правого верхнего в левый нижний угол, изменится лишь направление закрашивания.
Что еще? Да хоть килограмм! Давайте посмотрим заливку "веером". Смысл веера в том, что все линии проводятся из одного угла на стороны, противоположные ему. Длина массива здесь та же, что и в случае диагонально заливки.
Теперь посмотрим круговые виды: полуокружности сверху-снизу, слева-справа, заливка концентрическими окружностями от краев к центру.
В случаях полуокружностей длина массива под цвета должна быть равна половине ширины (в случае слева-справа) и половине высоты (в случае сверху-снизу). В случае с концентрическими окружностями - половине минимальной стороны заливаемой области, т.к. радиус окружности (или дуги) изменяется от нуля до центра заливаемой области. Надо сказать, что предварительно нужно залить область начальным цветом, чтобы была иллюзия того, что переход действительно плавен.
Вот функции:
function LeftRightPiesArrayWidth(FillRect: TRect): Integer; begin Result := VerticalArrayWidth(FillRect) div 2; end;
function TopBottomPiesArrayWidth(FillRect: TRect): Integer; begin Result := HorizontalArrayWidth(FillRect) div 2; end;
function CirclesArrayWidth(FillRect: TRect): Integer; var Width, Height, minus: Integer; begin Width := abs(FillRect.Right - FillRect.Left); Height := abs(FillRect.Bottom - FillRect.Top); minus := 15*(Width + Height) div Min(Width, Height); //величина minus определена чисто эмпирически, //возможно вы найдете лучше Result := Min(Width, Height) div 2+minus; end;
procedure TopBottomPiesGradient(Canvas: TCanvas; FillRect: TRect; Colors: TColorArray); var i: Integer; TempBmp: TBitmap; begin TempBmp := TBitmap.Create; TempBmp.Width := abs(FillRect.Right - FillRect.Left); TempBmp.Height := abs(FillRect.Bottom - FillRect.Top);
try With TempBmp do begin Canvas.Brush.Color := Colors[0]; Canvas.FillRect(FillRect); for i := 0 to TempBmp.Height div 2 do begin Canvas.Pen.Color := Colors[i]; Canvas.Brush.Color := Colors[i]; Canvas.Pie(0, - (TempBmp.Height div 2), TempBmp.Width, (TempBmp.Height div 2) - i, 0, 0, TempBmp.Width, 0);
Canvas.Pie(0, (TempBmp.Height div 2)+i, TempBmp.Width, 3*(TempBmp.Height div 2), 0, 0, 0, 0); end; end; Canvas.StretchDraw(FillRect,TempBmp); finally TempBmp.Free; end; end;
procedure LeftRightPiesGradient(Canvas: TCanvas; FillRect: TRect; Colors: TColorArray); var i: Integer; TempBmp: TBitmap; begin TempBmp := TBitmap.Create; TempBmp.Width := abs(FillRect.Right - FillRect.Left); TempBmp.Height := abs(FillRect.Bottom - FillRect.Top);
try With TempBmp do begin Canvas.Brush.Color := Colors[0]; Canvas.FillRect(FillRect);
for i := 0 to TempBmp.Width div 2 do begin Canvas.Pen.Color := Colors[i]; Canvas.Brush.Color := Colors[i]; Canvas.Pie(- TempBmp.Width div 2, 0, (TempBmp.Width div 2) - i, TempBmp.Height, 0, TempBmp.Height, 0, 0); Canvas.Pie((TempBmp.Width div 2) + i, 0, 3*TempBmp.Width div 2, TempBmp.Height, TempBmp.Width, 0, TempBmp.Width, TempBmp.Height); end; end; Canvas.StretchDraw(FillRect, TempBmp); finally TempBmp.Free; end; end;
procedure CirclesGradient(Canvas: TCanvas; FillRect: TRect; Colors: TColorArray); var i,Minus: Integer; TempBmp:TBitmap; begin TempBmp := TBitmap.Create; TempBmp.Width := abs(FillRect.Right - FillRect.Left); TempBmp.Height := abs(FillRect.Bottom - FillRect.Top); Minus := 15*(TempBmp.Width + TempBmp.Height) div Min(TempBmp.Width, TempBmp.Height); try With TempBmp do begin Canvas.Brush.Color := Colors[0]; Canvas.FillRect(FillRect); for i := 0 to CirclesArrayWidth(FillRect) do begin Canvas.Pen.Color := Colors[i]; Canvas.Brush.Color := Colors[i]; Canvas.Ellipse(Rect(i - Minus, i - Minus, TempBmp.Width - i + Minus, TempBmp.Height - i + Minus)); end; end; Canvas.StretchDraw(FillRect, TempBmp); finally TempBmp.Free; end; end;
Давайте теперь посмотрим заливку "конверт". Суть ее в том, что область закрашивается сходящимися в центр прямоугольниками. Длина массива здесь, также, должна быть равна половине минимальной стороны заливаемой области. Это нужно для того, чтобы был действительно эффект конверта. Кстати, мой любимый вид заливки :)
Вот эта процедура:
function EnvelopeArrayWidth(FillRect: TRect): Integer; var Width, Height: Integer; begin Width := abs(FillRect.Right - FillRect.Left); Height := abs(FillRect.Bottom - FillRect.Top); Result := Min(Width,Height) div 2; end;
procedure EnvelopeGradient(Canvas: TCanvas; FillRect: TRect; Colors: TColorArray); var i: Integer; TempBmp: TBitmap; begin TempBmp := TBitmap.Create; TempBmp.Width := abs(FillRect.Right - FillRect.Left); TempBmp.Height := abs(FillRect.Bottom - FillRect.Top);
try With TempBmp do for i := 0 to EnvelopeArrayWidth(FillRect) do begin Canvas.Brush.Color := Colors[i]; Canvas.FillRect(Rect(i, i, TempBmp.Width - i, TempBmp.Height - i)); end; Canvas.StretchDraw(FillRect, TempBmp); finally TempBmp.Free; end; end;
Ну, не будем раздувать и без того большую статью... Быстренько пробежимся по другим видам, которые я реализовал в своем модуле.
Заливка волнами. Длина массива - ширина (в случае горизонтальных волн) или высота (в случае вертикальных волн) заливаемой области. Кстати, частоту также можно задать. Но формула подобрана также эмпирически. Кстати, волны реализованы очень легко - заливаете битмап-полоску и потом в цикле рисуете градиентные полоски на i-ом расстоянии, равном синусу: Round(50*sin(Frequency*i). (Frequency - частота синусоиды).
Заливка звездой. Длина массива - чисто эмпирически подобрано - 2/3 минимальной из сторон. Ну, это сделано, чтобы звезда была побольше. В принципе, 2/3 можете убрать. Для построения звезды достаточно вспомнить геометрию;)
Заливка ромбом. Длина массива - такая же, что и в случае заливки конвертом.
Предела фантазии нет - все зависит только от вас. Можно комбинировать из уже имеющихся или придумать что-то новое. Мне после 16ти видов просто надоело... Надеюсь, статья не показалась вам скучной и подтолкнула на творческие поиски:)).
С уважением, Sega-Zero.
Скачать проект: (18K)
Немного теории.
Все цвета в двоичном коде представляются в виде трехбайтовых (или более) последовательностей. Есть различные схемы представления цвета - RGB, HLS, CMYK и некоторые другие, (например OpenGL) которые используются исключительно в системах компьютерной графики, нас они не очень интересуют. Итак, все по порядку. RGB (Red, Green, Blue) представляет цвет следующим образом: это трех байтовая последовательность вида $GGBBRR. Каждый байт представляет определенный оттенок (от 0 до 255) цветов: красный, зеленый, синий. Например, $FF0000 означает чистый синий цвет полной интенсивности, $00FF00 - чистый зеленый, $0000FF - чистый красный, $FFFFFF - черный цвет, $000000 - белый. Таким образом, задав определенное числовое значение, можно добиться того или иного оттенка искомого цвета. Но это в формате RGB. Чаще всего в Делфи мы пользуемся типом TColor, в котором добавлен еще 1 байт -указатель на замену цвета. Этот байт может принимать три различных значения - ноль ($00), единицу ($01) или двойку ($02). Что это значит:
Ноль ($00) - цвет, который не может быть воспроизведен точно, заменяется ближайшим цветом из системной палитры.
Единица ($01) - цвет, который не может быть воспроизведен точно, заменяется ближайшим цветом из палитры, которая установлена сейчас.
Двойка ($02) - цвет, который не может быть воспроизведен точно, заменяется ближайшим цветом из палитры, которую поддерживает текущее устройство вывода (в нашем случае - монитор).
Видимо, всегда лучше устанавливать значение первого байта равным нулю ($00), по крайней мере, так происходит при получении типа TColor при помощи функции RGB.
Схемой RGB пользоваться удобнее и привычнее, чем остальные, но расскажем немного и о них. Схема HLS(Hue, Light, Saturation) - Оттенок, Яркость, Насыщенность. Как видно из аббревиатуры, цвет представляется несколько иначе - через оттенок определенного цвета, его яркость и насыщенность. К сожалению, никогда этой схемой не пользовался, поэтому пример привести не могу:(. CMYK - Cyan-Magenta-Yellow-blacK палитра (голубой-сиреневый-желтый-черный), используется в издательских системах как более четко передающая цвета, чем палитра RGB. Также ничего не могу сказать, кроме того, что уже сказано. В моей статье мы будем рассматривать только модель RGB.
Теперь, что же такое градиент? Градиент - это плавный переход от одного цвета к другому. Очень хорошо градиент можно показать на примере радуги.
Допустим, вам нужно получить промежуточный цвет между красным и синим. Для этого просто находим среднее арифметическое отдельных координат RGB.
(255, 0, 0) + (0, 0, 255) = ((255+0)div 2, (0+0)div 2, (0+255)div 2) = (127, 0, 127).
То есть получили сиреневый цвет. Для случая многих цветов нужно будет сложить координаты всех цветов и разделить на их количество. Как же можно сделать этот плавный переход? Итак, пусть заданы 2 цвета своими координатами ((A1, A2, A3) и (B1, B2, B3)) и линия (длиной h пикселов), по которой нужно залить.
Тогда каждый цвет каждого пиксела, находящегося на расстоянии x пикселов от начала будет равен
(A1-(A1-B1)/h*x, A2-(A2-B2)/h*x, A3-(A3-B3)/h*x).
Теперь, наконец, перейдем, собственно, к реализации градиентной заливки.
Вступление.
Частенько приходится задумываться над тем, на сколько скуден стандартный интерфейс, насколько он безжизненнен (я не говорю про ХР - там все меняется к лучшему, но и там есть ляпы). Хочется сделать что-нибудь эдакое, грандиозное, чтобы глаз радовался. Представляю на ваш суд один из инструментов, который позволит скрасить серые будни программиста. Статья не претендует на фундаментальность - это просто зарисовка, эскиз к вашим будущим работам. Итак, начнем.
Добавление картинок и гиперссылок.
Бывает, что в справку нужно поместить изображения. Это можно сделать, просто добавив их в документ rtf обычным для MS Word способом. Если одно изображение используется в нескольких местах, то можно воспользоваться специальной разметкой, предусмотренной для вставки изображений в справку, так что изображение будет храниться в одном экземпляре. Здесь я не буду рассматривать, как это сделать (честно говоря, я этого никогда и не делал).
Обычное дело - указать адрес своего сайта и/или электронной почты в разделе "Поддержка". Как сделать, чтобы эта ссылка выглядела и функционировала как гиперссылка на Веб-страницах? Аналог гиперссылки, т.е. тега anchor (<a>) HTML в WinHelp - "hotspot". Hotspot - это область, по щелчку мыши на которой, выполняется какое-либо действие из предусмотренных в WinHelp. В данном случае нам нужно запустить один из макросов WinHelp, а именно макрос ExecFile. Этот макрос напоминает функцию WinApi ShellExecute. Синтаксис его такой:
ExecFile(program[, arguments[, display-state[, topic-ID]]])
Первый параметр, как написано в справке, может указывать на программу или файл. Однако, как и в ShellExecute, вместо имени файла можно указать URL, например "http://www.mysite.ru" или "mailto:nekto@somemail.ru".
Чтобы создать hotspot, запускающий такой макрос, нужно сделать следующее:
Сразу после текста hotspot'a ввести символ "!", а сразу за ним - текст макроса, например:
Наш сайт: www.mysite.ru!ExecFile(http://www.mysite.ru)
Примечание: URL в скобках должен быть без кавычек.
Далее, нужно отформатировать этот отрывок так: текст hotspot'a должен иметь двойное подчеркивание, а символ "!" и текст макроса после него - иметь атрибут "скрытый". И то, и другое делается через меню "Формат" -> "Шрифт" (см. рис. 2) На всякий случай, еще раз уточню: двойное подчеркивание (выпадающий список рядом с "цветом текста"), а не зачеркивание..
Рис. 2.
Теперь, если добавить такой hotspot и компилировать проект, то мы увидим в своей справке, что в строке текста
Наш сайт: www.mysite.ru - адрес выглядит и функционирует как гиперссылка.
Общие сведения
В состав операционной системы Windows входит подсистема поддержки справки - WinHelp. Она имеет свое API, которое позволяет предоставить справочную информацию в том или ином виде в приложениях Windows. Создание справочной системы можно разделить на две части
Создание собственно справки, ее содержания
Подключение справки к программе - то есть обеспечение доступа к справочной информации удобным для пользователя способом.
В состав справки к программе обычно входит несколько файлов:
Файлы содержащие собственно информацию - имеют расширение .hlp
Файлы оглавления - .cnt (от апглийского content)
После первого вызова справки WinHelp создает файл .gid
Также другие типы файлов, например, для полнотекстового поиска, о чем в статье речь не идет.
Пишем простой хелп
Сергей Осколков,
Справочная система (далее в тексте также - "справка") - важная составная часть любой более-менее сложной (и даже простой) программы. Существуют разные форматы справочных систем. Справка в формате WinHelp - о ней в статье и пойдет речь, Html-help - как, например, справка к ОС Windows или к программам MS Office. В качестве справки может выступать набор связанных html-файлов, например так организована справка к СУБД MySQL. Из достоинств WinHelp можно назвать ее надежность и меньшие, чем у HTML-help, требования к ресурсам. Существуют различные программы для создания справочных систем названных типов. Однако, для создания несложной справки вполне достаточно стандартной программы MS Help Workshop, которая поставляется с Delphi. Потребуется еще редактор rtf файлов, в данной статье описывается работа со знакомым всем MS Word'ом. Все действия, которые будут описаны - несложные, но мне в свое время потребовалось определенное количество усилий и времени, чтобы разобраться по справке к Help Workshop, как все это делать. Надеюсь, что статья сможет облегчить этот путь для других. Расчитана она на начинающих. В статье описывается создание простой справки, оглавления к ней, создание последовательностей просмотра, вставка рисунков и гиперссылок, а также подключение справки к программе. Рассматривается только техническая сторона, вопрос о том, что написано в справке, оставлен в стороне.
Присоединяем справку к программе.
Чтобы справка была доступна в программе, нужно указать программе на файл .hlp. Самый простой способ размещения файла справки - в той же папке, где находится исполняемый файл. Определить для приложения файл справки можно так: открыв проект (приложения) в Delphi и выбрав меню "Project" -> "Options" -> "Application", вписать название файла справки в поле ввода "Help file". При этом нужно указать название файла без пути. Когда WinHelp пытается найти справочный файл, одна из просматриваемых директорий - та, где расположен исполняемый файл программы. Другой способ - в обработчик события OnCreate главной формы программы вставить строку:
Application.HelpFile:=ExtractFilePath(Application.ExeName) + "MyHelp.hlp";
где MyHelp.hlp - название файла справки.
Чтобы из меню в программе вызвать оглавление справки, нужно воспользоваться функцией
Application.HelpCommand(HELP_FINDER, 0);
Чтобы перейти к одной из определенных нами тем справки, нужно вызвать функцию
Application.HelpJump('MyTopic'),
где MyTopic - идентификатор темы.
Один из способов вызова справки - нажатие клавиши F1. Можно организовать вызов контекстной справки при нажатии на F1, когда активным является тот или иной элемент управления. Для этого соответствующей теме справки нужно присвоить номер, а затем этот номер присвоить свойству HelpContext элемента управления. Чтобы задать номера для тем справки, нужно открыть проект справки в HelpWorkshop и нажать кнопку "Map" в правой части окна. Нажимаем в диалоговом окне "Add", вводим идентификатор темы и произвольный номер (например, 1 :) ), повторяем это для всех нужных тем (каждой - свой номер), закрываем окно и нажимаем в очередной раз "Save and Compile". Затем в Delphi, в окне инспектора объектов, присваиваем нужные номера нужным элементам управления (напоминаю, свойство HelpContext).
Создание оглавления справки.
Файл оглавления справки имеет простой текстовый формат, но создавать его удобно тоже в HelpWorkshop. Для этого выбираем в меню HelpWorkshop "File" -> "New" -> "Help Contents". В верхней части окна нужно вписать имя главного файла справки (файлов, вообще говоря, может быть несколько) и заголовок (title) для оглавления справки. То же можно сделать в диалоговом окне, которое открывается при нажатии кнопки Edit... .
Теперь создаем собственно оглавление. Оно состоит из элементов двух типов - разделы справки, которые включают в себя несколько тем и представлены в оглавлении справки значком книжки и сами темы - текст и картинки, представлены в оглавлении справки значком листа со знаком вопроса на нем (можно посмотреть это в оглавлении любой справки). Также в оглавление можно вставить макросы и включить файлы (include), этого я здесь касаться не буду. Справа на панели есть набор кнопок для добавления и манипуляции элементами оглавления. (Add Below - Добавить ниже, Add Above - Добавить выше, Move Right - Сдвинуть вправо, Move Left - Сдвинуть влево, Edit, Delete). При помощи них создаем нужную структуру оглавления. При добавлении раздела в диалоговом окне нужно указать только его название, при добавлении темы - название, идентификатор (тот, который мы задали ей в rtf-файле, когда вставляли концевую сноску), имя help-файла и имя окна. Если тема находится в том же help-файле, который мы указали как главный, то имя help-файла указывать не нужно. Имя окна указывать тоже не обязательно, если оно не указано, то тема откроется в окне по-умолчанию. Нужно сохранить файл оглавления (он будет иметь расширение .cnt) в той же директории, где находится help-файл лучше с тем же именем, что и help-файл. Теперь нужно снова открыть файл проекта .hpj и, нажав кнопку "Options", в открывшемся диалоговом окне на закладке "Files" указать наш файл оглавления (Contents file). Закрываем диалоговое окно, снова нажимаем "Save and Compile". Теперь при двойном щелчке мышью по значку файла справки должно открыться ее оглавление, из которого можно получить доступ ко всем темам.
Мне не удалось, похоже, это невозможно, создать такую структуру оглавления, чтобы в самом левом ряду сначала шел значок темы (например, "Общие сведения"), а под ним - значки разделов. Пришлось даже для одиночной темы создавать раздел, содержащий ее одну.
Создание последовательностей просмотра.
Удобно перемещаться в справке от темы к теме с помощью кнопок ">>" и "<<" на панели инструментов окна или при помощи клавиш "<", ">" клавиатуры. Чтобы добавить такую возможность, нужно:
Создать одну или несколько последовательностей просмотра (browse sequence) в .rtf файле текста справки, добавив соответствующую разметку.
Определить в проекте справки (файл .hpj) окно с соответствующими кнопками.
Разметка имеет следующий вид: это тоже концевые сноски, как и для идентификаторов тем, однако в данном случае знаком сноски служит не символ "#", а "+" - знак плюса. Текстом сносок может быть либо число, либо строка символов. Просмотр будет осуществляться в порядке возрастания (как при сортировке строк). Отсюда следующее - если используются номера, то нужно вставлять необходимое количество нулей перед значащими цифрами для правильной сортировки. Например, если у Вас 20 тем, то первые нужно нумеровать как 01, 02, и т.д. Возможно несколько вариантов:
Если последовательностей несколько, то каждая из них может иметь имя, а темы внутри последовательности отличаться номерами (например, GUI1, GUI2, GUI3, ...), Если номера не заданы, WinHelp сам создаст последовательность просмотра при компиляции проекта в том порядке, как расположены темы в файле .rtf.
Если не писать ничего в текстах ссылок или написать во всех ссылках "auto" (без кавычек), то WinHelp при компиляции создаст одну последовательность просмотра в том порядке, как расположены темы в файле .rtf.
Чтобы добавить кнопки навигации "<<" и ">>" в окне справки (по умолчанию их нет), нужно определить хотя бы одно окно. Для этого, открыв файл проекта в HelpWorkshop, нужно нажать кнопку "Windows..." в правой части окна. В диалоговом окне с закладками нажать кнопку "Add..." и в открывшемся окне ввести в поле названия "main", а поле типа окна очистить, после чего нажать OK. Теперь у нас определено одно окно, различные свойства которого можно изменять, перемещаясь по закладкам. На закладке "Buttons" отмечаем галочкой "Browse". Нажимаем ОК, готово. Теперь окно справки будет иметь нужные кнопки. Нажимаем "Save and Compile" внизу окна и можем проверять, что получилось в выходном help файле.
Создание тематических файлов.
Для создания справки сначала нужно подготовить файлы, содержащие справочную информацию. Это - обычные документы в формате rtf, которые можно создавать с помощью различных программ. Я здесь буду описывать работу с MS Word (создаем документ и выбираем "Сохранить как" - "Текст в формате RTF"). Будем называть эти файлы тематическими (topic files). Для получения из тематических файлов готовых файлов справки (hlp), их нужно обработать (компилировать) программой HelpWorkshop (hwc.exe).Кроме собственно текста и рисунков, тематические файлы могут содержать также специальную разметку, которая несет в себе информацию, нужную для создания переходов по ссылкам, связи оглавления справки с ее темами и т.п. - для реализации различных возможностей WinHelp.
Обычно справка содержит несколько тем и оглавление, из которого можно перейти к этим темам. Самый простой вариант: если тема - одна и оглавления нет. В таком случае просто пишем то, что нужно и сохраняем это в файле с расширением ".rtf". Для создания нескольких тем процесс усложняется ненамного:
Каждая тема должна заканчиваться жестким переходом на новую станицу. Для этого после окончания темы нужно в меню (напомню, что речь идет о MS Word) "Вставка" выбрать "Разрыв"->"Начать новую страницу".
Для того, чтобы тема были доступна из оглавления к справке, нужно задать ей идентификатор. Для этого нужно в то место текста, куда будет впоследствии происходить переход из оглавления (начало темы или, если нужно, другое место), вставить специальную разметку, а именно: концевую сноску. Символом сноски нужно выбрать "#". Идентификатором темы служит текст сноски. Например, создадим тему "Поддержка", отделим ее от других тем разрывами страниц и зададим ей идентификатор "support". Для этого поместим каретку ввода около заголовка темы и выберем в меню "Вставка"->"Сноска...". В диалоговом окне (рис.1) выбираем вид сноски - "концевая", нумерация - "другая", в окошке для ввода символа пишем "#" (без кавычек, понятное дело). Нажимаем ОК, ссылка добавлена и каретка автоматически переведена к тексту ссылки. Пишем "support". Готово.
Повторяем то же самое для всех тем справки. Сохраняем файл. Теперь можно попробовать создать свой хелп.
Рис.1. Добавление концевой сноски.
Запускаем программу HelpWorkshop. Это - файл Hcw.exe в директории Delphi\Help\Tools\. Создаем новый проект через меню "File"->"New"->"Help Project". Справа на панели есть ряд кнопок, нажимаем "Files...". В диалоговом окне добавляем наш тематический файл и закрываем это окно. Сохраним проект - это будет файл с расширением hpj (Help Project). Насколько я понял, после первого запуска Help Workshop связывает себя с файлами hpj, а также - с файлами оглавления справки (cnt), так что их потом можно открывать двойным щелчком мышью. Для создания help-файла можно просто нажать кнопку "Save and Compile". Откроется новое окно с сообщением о результате компиляции. Предположим, что все в порядке, закроем это окно. Теперь в директории, где находился наш проект (.hpj), должен появиться файл справки. Однако, при двойном щелчке мышью на нем мы сможем просмотреть только первую тему. Чтобы просматривать все темы и перемещаться между ними, нужно добавить файл оглавления.
Безобидные(?) Hints
Ниже приведен код простой функции, которая не содержит синтаксических ошибок, но при её компиляции будет получено три Hint'а (в коде они отмечены красным). Давайте разберем их подробно.
Function FunctonName( Code : String) : Integer; Var i,j : Integer; ‹—— Variable 'i' is declared but never used in 'FunctonName'
Begin j:=0; ‹—— Value assigned to 'j' never used
For j:=0 To -1 Do Writeln(j); ‹—— FOR or WHILE loop executes zero times - deleted
Result:=StrToInt(Code); End; |
Variable 'i' is declared but never used in 'FunctonName'
Переменная 'i' определена, но никогда не используется в 'FunctonName' — это одно из самых часто встречающихся сообщений. Чаще всего оно просто говорит о неаккуратном коде. Однако, наличие таких переменных в принципе может означать потенциальную ошибку в реализации алгоритма, ведь зачем-то она была объявлена. Именно поэтому компилятор обращает ваше внимание на эту переменную: вдруг вы просто забыли доделать задуманное?
Простейшее решение — удалить все неиспользованные переменные. А заодно и проверить, действительно ли они не нужны :о)
Value assigned to 'j' never used Значение, присвоенное 'j' никогда не используется. Это сообщение не означает, что программа неправильная — оно означает только то, что компилятор обнаружил, что после присвоения переменной j значения 0, эта переменная не участвует более ни в каких операциях. Что делает это присвоение абсолютно бессмысленным. И, если используется оптимизатор, оно будет удалено в откомпилированном коде.
Так же, как и предыдущий Hint, это сообщение чаще всего является признаком "мусора" в коде, от которого следует избавляться, чтобы не пропустить потенциальные ошибки. Опасность в том, что в реальности может оказаться, что это присвоение было сделано не той переменной, которой нужно. Например, надо было присвоить что-то переменной i, а присвоили j.
FOR or WHILE loop executes zero times - deleted
Цикл FOR или WHILE исполняется ноль раз — удалено. Собственно, текст этого сообщения полностью объясняет ситуацию. Конечно же это не специально, это "рука дрогнула", "глаз замылился" или что-то в таком духе. И компилятору остается только сказать спасибо.
Итак, получается, что Hint'ы обращают наше внимание на странности и несуразицы в коде с точки зрения "правильного" компилятора. Конечно, приведенный пример очень прост и надуман и может не убедить вас, но если в коде функции, которая содержит не один десяток операторов, появляется hint, стоит обратить на него внимание, поверьте.
Рассмотренные выше ситуации можно и нужно исправлять. Но бывают случаи, когда нет возможности исправить код так, чтобы не получать Hint's при компиляции. Рассмотрим небольшой пример по созданию собственных классов:
Type TLists = class(TList) Protected procedure Clear; override; ‹—— Overriding virtual method 'TLists.Clear' has a lower visibility (private) than base class (public)
End;
TExLists = class(TList) Private Function FutureTools(Sender : TObject) : Boolean; ‹—— Private symbol 'FutureTools' declared but never used
Public ... End; |
Overriding virtual method 'TLists.Clear' has a lower visibility (private) than base class (public)
Переопределенный виртуальный метод 'TLists.Clear' имеет видимость ниже, чем в базовом классе
Это не то, чтобы ошибка, но на практике понижение видимости свойств и методов класса встречается довольно редко и говорит об ошибках на этапе проектирования базовых классов. Это понижение видимости может создать в проблему в будущем, если от класса TLists будут наследоваться при создании новых классов.
Private symbol 'FutureTools' declared but never used
Приватный символ 'FutureTools' определен, но никогда не используется. Это сообщение сродни уже описанному Variable '<name>' is declared but never used...
Так как этот метод приватный, то он по определению не может быть доступен нигде более, как внутри класса. Тем не менее, компилятор там его использования не обнаруживает. Из чего следует естественный вывод, что функция 'FutureTools' нигде не будет использоваться.
Допустим, что в этом случае все не так просто, как это видится компилятору и функция FutureTools, например, нигде не используется вовсе не потому, что вы о ней забыли или она никому не нужна. Возможно это задел на будущее. Можно, конечно, закомментировать и объявление функции и код ее реализации до поры до времени. Но можно сделать и иначе, несколько изящнее.
Возможно, что по условию конкретной задачи понижение видимости метода в классе TLists оправдано, а корректировать код базового класса нет возможности, тогда придется попросить компилятор не принимать во внимание эту ситуацию.
Как раз для таких случаев предусмотрена сцециальная дирректива компилятора: {$HINTS OFF}. Она отключает проверку кода на предмет обнаружения Hint'ов до тех пор, пока в коде не встретится обратная дирректива — {$HINTS ON}. Если в обрамленном этими специальными комментариями коде и будут "опасные" Hint-ситуации, они будут игнорироваться компилятором.
Воспользовавшись этими диррективами, мы получим код, который компилируется не только без ошибок, но и без Hint'ов:
Type {$HINTS OFF}
TLists = class(TList) Private procedure Clear; override; End; {$HINTS ON}
TExLists = class(TList) Private {$HINTS OFF}
Function FutureTools(Sender : TObject) : Boolean; {$HINTS ON}
Public ... End; |
Примечание:
Не поддавайтесь искушению раз и навсегда "заткнуть" с помощью {$HINTS OFF} упрямый компилятор, пользы от этого вам, как программисту, не будет никакой...
Hints and Warnings, или Спасение утопающих
Елена Филиппова, Королевство Дельфи
15 апреля 2003г.
Содержание:
Каждая программа содержит по крайней мере одну ошибку
Народная мудрость
Никогда не делает ошибок тот, кто просто ничего не делает. Это тоже народная мудрость. Поэтому с ошибками в коде сталкивается в своей работе каждый программист. После того, как программа успешно откомпилирована, преодолен первый этап борьбы. :о)
Не секрет, что гораздо сложнее бороться с ошибками, возникающими во время выполнения программы, особенно, когда они приводят не просто к ее "падению", а к неадекватной работе, наслаивая проблемы и создавая "наведенные" ошибки. И здесь уже надежды на компилятор нет... Спасение утопающих, как известно, дело рук этих самых утопающих.
Материал данной статьи не имеет отношения к теме тестирования и отладки. Он предназначен начинающим программистам, дабы обратить их внимание на "соломинку", которую протягивает утопающим IDE Delphi в нелегком деле борьбы с ошибками :о) Ведь не зря же ее называют дружественной средой разработки.
Хочу сразу обратить ваше внимание на то, что все приводимые примеры не являются реальными, они специально упрощены и только иллюстрируют объяснение материала.
Коварные Warnings
Предупреждения-warnings обладают гораздо более высоким уровнем опасности с точки зрения компилятора. История с абстрактным классом служит тому примером. Разберем еще несколько случаев возникновения warning'ов:
Return value of function 'VarCompare' might be undefined
Значение результата функции 'VarCompare' может быть неопределено.
Function VarCompare(Index1, Index2: Integer): Integer; Begin IF Index1 = Index2 Then Result:=0; IF Index1 < Index2 Then Result:=-1; IF Index1 > Index2 Then Result:=1; End; ‹——Return value of function 'VarCompare' might be undefined
|
Казалось бы, с точки зрения логики в тексте функции все верно. Перекрыты все возможные случаи и сообщение компилятора выглядит несколько неуместно. Но не стоит ждать от него слишком много, компилятор не может (да и не обязан) вникать в логику программы. Для того, чтобы избавиться от этого сообщения, было бы правильно переписать это код. Например, вот так:
Function VarCompare(Index1, Index2: Integer): Integer; Begin IF Index1 = Index2 Then Result:=0 Else IF Index1 < Index2 Then Result:=-1 Else Result:=1; End; |
В итоге и компилятор "отстанет", и код будет более читабельным. Это сообщение только на первый взгляд кажется безобидным, ниже приведен пример, в котором возникает аналогичное предупреждение и содержится реальная ошибка — если возникнет исключительная ситуация при открытии файла, результат функции, действительно, не будет определен. В итоге это скажется при выполнении программы, когда ошибки никто не будет ожидать.
Function ReadList( FileName : String) : Boolean ; Var Stream : TFileStream; Begin IF FileExists(FileName) Then Try Stream:=TFileStream.Create(FileName , fmOpenRead); // .....
Stream.Free; Result:=True; Except End Else Result:=False;
End;‹——Return value of function 'ReadList' might be undefined
|
Правильный вариант:
Function ReadList( FileName : String) : Boolean ; Var Stream : TFileStream; Begin IF FileExists(FileName) Then Try Stream:=TFileStream.Create(FileName , fmOpenRead); // .....
Stream.Free; Result:=True; Except Result:=False; End Else Result:=False;
End; |
Еще один пример коварного warning'а:
Variable 'list' might not have been initialized
Переменная 'list' может быть не инициализирована.
Function SomethingList( Text : String) : Integer; Var list : TStringList; Begin IF Text <> '' Then Begin list:=TStringList.Create; list.CommaText:=Text; End;
// .... код
Result:=list.Count; ‹—— Variable 'list' might not have been initialized
list.Free; End; |
Совершенно справедливое замечание. Если во время работы программы в функцию будет передана пустая строка, нам обеспечен знаменитый Access violation.
Вернемся еще раз к примеру с определением собственных классов.
TExLists = class(TList) Public procedure Clear; ‹—— Method 'Clear' hides virtual method of base type 'TList'
End; |
Method 'Clear' hides virtual method of base type 'TList' Метод 'Clear' прячет виртуальный метод базового класса 'TList'. Эта ситуация буквально означает перекрытие виртуального метода родительского класса. То есть, в классе TExLists определен статический метод, имя которого совпадает с виртуальным методом родительского класса TList. Если в дальнейшем, от класса TExLists будут наследоваться, то метод Clear для этих наследников будет закрыт.
Правильный вариант:
TExLists = class(TList) Public procedure Clear; override; End; |
Точно также, как и в случае с hint'ами, существуют опции для отключения сообщений компилятора о предупреждениях — {$WARNINGS OFF}, и для их включения — {$WARNINGS ON}. И точно так же хочу обратить внимание на нежелательность использования этих опций без нужды. Молчание компилятора в этом случае не будет означать отсутствие проблемы :о)
О пользе сообщений компилятора
Небольшое лирическое отступление:
В каждом уважающем себя форуме есть список вопросов, признанных как off-topic. Часть из них сто раз уже разжевана, часть решается нажатием клавиши F1 и так далее. На каждом форуме борятся с ними по-своему, но, к огромному сожалению, задающих такие вопросы не становится меньше. Более того, вопрошающие частенько еще и обижаются, когда их отсылают :о) Вот пример классического off-topic'а:
Привет Алл! Пишу код
s:tstrings; s:=tstrings.create; s.insert(... // здесь ОШИБКА! Какой-то Abstract Error s.clear;
Господа подскажите что делать? |
В ответ на такой вопрос, господа, как правило, начинают страшно ругаться. :о) Самые вежливые слова, которые получает автор вопроса, звучат примерно так — "Сколько же можно?! Хелп когда научитесь читать?!" На что автор, как ему кажется, абсолютно справедливо, начинает огрызаться, что типа, откуда ему было знать, что такое абстрактный метод и что на этом самом TStrings не написано, какие у него методы!
Проведем маленький эксперимент и напишем такой код:
Procedure AbstractMethod; Var Buffer : TStrings; Begin
Buffer:=TStrings.Create; ‹—— Constructing instance of 'TStrings' containing abstract methods
Buffer.LoadFromFile('test.txt'); Buffer.Free;
End; |
При компиляции нам будет выдан warning, как раз на той строке, где создается экземпляр класса — Constructing instance of 'TStrings' containing abstract methods. Я надеюсь, что текст этого предупреждения абсолютно ясен и не требует пояснений...
Смотрите, что получается, ошибок компиляции нет, человек с высоко поднятой головой игнорирует "всю эту ерунду" и просто не обращает внимания на предупреждения компилятора! В итоге, он получает ошибку времени выполнения, некоторое личное недоумение, кучу словесных тычков и подзатыльников на форуме. А ведь его предупреждали! :о)
рис. 2
IDE Delphi, как дружественная среда программирования, кроме обычного факта уведомления о сообщениях компилятора, предоставляет дополнительные возможности — если дважды кликнуть на тексте сообщения (рис. 2), то курсор автоматически переместиться на ту строку в редакторе кода, в которой, по мнению компилятора, возникает спорная ситуация. Если же на тексте сообщения (hint или warning) нажать F1, то откроется окно справочной системы (рис. 3) по конкретному hint'у или warning'у. Там будет описано, в каких случаях компилятор выдает такое сообщение и что Delphi вообще "думает" по этому поводу.
рис. 3
Цель этого материала, не рассказать
Цель этого материала, не рассказать обо всех возможных hint'ах и warning'ах, их список слишком велик для одной статьи, а обратить внимание на необходимость анализировать ВСЕ сообщения компилятора в ваших программах.
Елена Филиппова
Специально для
Типы сообщений компилятора
Информацию о результате компиляции и сборки программы можно увидеть в окне, показывающем процесс компиляции (рис. 1), и на панели сообщений, встроенной в редактор кода (рис. 2).
рис. 1 Сообщения компилятора бывают трех типов. В этом списке они приведены по убыванию степени опасности, если так можно выразиться :о)
Error — ошибка
Warning - предупреждение
Hint - подсказка или совет.
Довольно распространенное отношение начинающих программистов к этим сообщениям заключается в полном игнорировании предупреждений и советов. Ведь не ошибки же? Программа откомпилирована и запускается на исполнение. И, может быть, даже работает :о)
Мне приходилось встречать на некоторых форумах "дружеские советы" новичкам, сводившиеся к предложению "не обращать на эту ерунду внимания, это оптимизатор у Delphi выделывается."
Так ли это на самом деле?
При наличии в проекте ошибок-Errors, не будет сформирован исполняемый файл и, волей не волей, ошибки придется исправлять. Наличие же сообщений Hints и Warnings позволяет запускать приложение. Обратите внимание на окно процесса компиляции (рис. 1), в строке "Done" написано не Compiled, что, в общем-то, ожидалось, а предупреждение There are warnings. Несмотря на отсутствие ошибок, проект откомпилирован с тремя "подсказками" и пятью "предупреждениями".
Насколько безопасно не обращать на это внимание?
Начнем с самых безобидных сообщений компилятора, с его советов — Hints.
Иконки в трейбаре? Проще чем кажется!
Михаил Продан,
Во многих форумах с завидной систематичностью выплывают вопросы об иконках в трее и многие программеры с недовольством отвечают: "Это же так просто, почитай документацию". Да, действительно просто - но лучше все посмотреть на практике, в человеческом, так сказать, изложении. Эта статья как раз и предназначена для заполнения некоторого "информационного вакуума" сложившегося по данной теме и расскажет о некоторых приемах работы с треем в Delphi.
Описание параметров:
cbSize - как и одноименные параметры из других системных структур, задает размер структуры TnotifyIconData; Wnd - идентификатор окна, которое будет реагировать на сообщения нашей иконки; UID - идентификатор, по которому Wnd определяет нашу иконку; UFlags - флаги, которые могут принимать значения NIF_ICON, NIF_MESSAGE, NIF_TIP; Icon - идентификатор иконки, которая будет размещена в трее; SzTip - строка всплывающей подсказки.
Определения
Итак, tray - это область рабочего стола Explorer'а, которая находится в одном из углов экрана, "там где часы". Вся информация, которую можно "почерпнуть" из трея, представлена в виде возможно изменяющихся иконок, отражающих состояние программы, и всплывающих подсказок. К числу активных действий над иконкой в трее можно отнести щелчок левой кнопкой и вызов контекстного меню правой.
Параметр - тип
CbSize - DWORD
Wnd - HWND
UID - UINT
UFlags - UINT
UCallbackMessage - UINT
Icon - HICON
SzTip - Array [0..64] of AnsiChar
С чего начать
Для операций с иконками трея используется только одна функция Windows - Shell_NotifyIconData, определение которой "звучит" следующим образом:
function Shell_NotifyIcon (dwMessage: DWORD; lpData: PNotifyIconData): BOOL; stdcall;
И в качестве параметров функция воспринимает: dwMessage - идентификатор сообщения, которое посылается иконке в трее. Может принимать значения NIM_ADD,NIM_DELETE,NIM_MODIFY; lpData - указатель на структуру TNotifyIconData, в которой помещена информация о параметрах иконки; возвращаемое значение - принимает true при успешном завершении операции или false в противном случае.
Как видно, сам синтаксис функции не сказал нам ничего нового о том, как реализована работа с треем. Эта функция только изменяет состояние иконки в зависимости от значения параметра dwMessage - а это значит, что вся нужная нам информация находится в структуре TNotifyIconData. Давайте теперь рассмотрим ее более детально…
Tray в чистом виде
А теперь приступим к собственно выводу иконки в трей. Для начала создадим форму, где все это разместим:
type
TForm1 = class (TForm)
Button1: TButton;
procedure Button1Click (Sender: TObject);
procedure FormDestroy (Sender: TObject);
private
procedure TrayDefaultHandler (var Message:TMessage);
{Private declarations}
public
Data:TNotifyIconData;
{Public declarations}
end;
потом - кнопку TButton, в которой запишем:
procedure TForm1.Button1Click (Sender: TObject);
var H:THandle;
begin
H:=AllocateHWnd (TrayHandler);
FillChar (S,SizeOf (S),#0);
Data.cbSize:=SizeOf (S);
Data.Wnd:=H;
Data.uCallbackMessage:=WM_TRAYICON;
Data.uFlags:=NIF_ICON or NIF_TIP or NIF_MESSAGE;
Data.hIcon:=Application.Icon.Handle;
StrPCopy (data.szTip,Application.Title);
Shell_NotifyIcon (NIM_ADD,@data);
end;
Небольшие пояснения. Во-первых, мы создаем постое окно с дескриптором H, которое будет реагировать на сообщения иконки. После этого очищаем предопределенную структуру Data типа TNotifyIconData, затем заполняем необходимые поля. Значение поля uFlags представляют собой уведомление системы о том, что ей надо использовать. Так, использование значения NIF_ICON уведомляет систему о том, что в поле hIcon присутствует непустое значение, которое надо вывести в виде иконки; использование значения NIF_TIP говорит о наличии текста всплывающей подсказки в поле szTip; значение NIF_MESSAGE - о том, что в поле Wnd присутствует дескриптор окна, которому передается управление при возникновении того или иного сообщения у иконки.
После заполнения всех необходимых полей вызывается функция Shell_NotifyIcon со значением NIM_ADD - добавление иконки в трей.
Теперь рассмотрим реакцию иконки на сообщения:
procedure TForm1.TrayDefaultHandler (var Message:TMessage);
begin
if Message.Msg=WM_TRAYICON then
if Message.LParam=WM_LBUTTONDOWN then
begin
ShowMessage ('Left Button Down');
end;
end;
Как видно из текста, здесь в качестве реакции реализован простой вывод уведомления о нажатии левой кнопки мыши на иконке. Идентификатор WM_TRAYICON, используемый здесь, определен нами в модуле главной формы следующим образом:
const WM_TRAYICON = WM_USER + 1;
такое определение необходимо для того, чтобы сообщения системы не перекрывались.
После того как мы убедились в наличии реакции и хотим закрыть приложение, нам надо удалить нашу иконку из трея, так как, если мы этого не сделаем, то она останется лежать там до следующей перегрузки Explorer'а.
Удаление иконки реализуется таким кодом:
procedure TForm1.FormDestroy (Sender: TObject);
begin
Shell_NotifyIcon (NIM_DELETE,@data);
end;
Здесь нам даже не потребовалось никаких вмешательств в структуру data - мы просто вызвали Shell_NotifyIcon с необходимым параметром, как показано ниже:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics,
Controls, Forms, Dialogs, StdCtrls;
const UWM_TRAYICON = WM_USER+1;
const ID_TRAYICON = 1;
type
TForm1 = class (TForm)
Button1: TButton;
procedure Button1Click (Sender: TObject);
procedure FormDestroy (Sender: TObject);
private
procedure TrayDefaultHandler (var Message:TMessage);
{Private declarations}
public
data:TNotifyIconData;
{Public declarations}
end;
var
Form1: TForm1;
implementation
Uses ShellApi;
{$R *.dfm}
procedure TForm1.TrayDefaultHandler (var Message:TMessage);
begin
if Message.Msg=UWM_TRAYICON then
if Message.LParam=WM_LBUTTONDOWN then
begin
ShowMessage ('Left Button Down');
end;
end;
procedure TForm1.Button1Click (Sender: TObject);
var H:THandle;
begin
H:=AllocateHWnd (Self.TrayDefaultHandler);
FillChar (S,SizeOf (S),#0);
data.cbSize:=SizeOf (S);
data.Wnd:=H;
data.uCallbackMessage:=UWM_TRAYICON;
data.uFlags:=NIF_ICON or NIF_TIP or NIF_MESSAGE;
data.hIcon:=Application.Icon.Handle;
StrPCopy (data.szTip,Application.Title);
Shell_NotifyIcon (NIM_ADD,@data);
end;
procedure TForm1.FormDestroy (Sender: TObject);
begin
Shell_NotifyIcon (NIM_DELETE,@data);
end;
end.
Эта небольшая заметка лишь слегка
Эта небольшая заметка лишь слегка приоткрывает занавес над таким обширным полем для деятельности, как иконки в трейбаре. Вообще же в этой области создано немало чудных вещей - например, компоненты с возможностью анимации (как в The Bat!) и прочими "вкусностями".
Кроме того, на иконку, как правило, навешивается меню по правой кнопке - для краткости примера здесь не показано, как это сделать, но, надеюсь, это и так ясно. Как говорится, "нет предела совершенству" - так что дерзайте!
document.write('');
|
|
|
|
|
|
|
|
Новости мира IT:
02.08 - 02.08 - 02.08 - 02.08 - 02.08 - 01.08 - 01.08 - 01.08 - 01.08 - 01.08 - 01.08 - 01.08 - 01.08 - 01.08 - 01.08 - 31.07 - 31.07 - 31.07 - 31.07 - 31.07 -
Архив новостей
|
|
|
|
Последние комментарии:
(66)
2 Август, 17:53
(19)
2 Август, 17:51
(34)
2 Август, 15:40
(42)
2 Август, 15:35
(1)
2 Август, 14:54
(3)
2 Август, 14:34
(3)
2 Август, 14:15
(2)
2 Август, 13:34
(7)
2 Август, 13:04
(3)
2 Август, 12:28
|
|
|
BrainBoard.ru
Море работы для программистов, сисадминов, вебмастеров.
Иди и выбирай!
|
|
|
|
Loading
google.load('search', '1', {language : 'ru'}); google.setOnLoadCallback(function() { var customSearchControl = new google.search.CustomSearchControl('018117224161927867877:xbac02ystjy'); customSearchControl.setResultSetSize(google.search.Search.FILTERED_CSE_RESULTSET); customSearchControl.draw('cse'); }, true);
|
|
|
|
|
IT-консалтинг |
Software Engineering |
Программирование |
СУБД |
Безопасность |
Internet |
Сети |
Операционные системы |
Hardware |
| PR-акции, размещение рекламы — , тел. +7 495 6608306, ICQ 232284597
| Пресс-релизы —
|
|
|
|
|
This Web server launched on February 24, 1997
Copyright © 1997-2000 CIT, © 2001-2009 |
Внимание! Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав. |
Если Вы решили приобрести , тогда Вам просто необходимо воспользоваться предложением от нашей компании.
|
Вариант передачи параметра в хранимую процедуру, использующую оператор IN
Нариман Курбанов (_MaSteR_NN_),
В этой статье мы рассмотрим возможность передачи параметров в хранимую процедуру, использующую оператор IN. Изложение материала будет вестись на базе тестового примера, который мы будем обсуждать по ходу описания данной статьи.
Используется: СУБД MSSQL 2000, Delphi7, ADO.
Итак, начнём:
Поставим перед собой задачи:
Создание тестовых табличек на сервере.
Создание удобного (ИМХО) и понятного интерфейса для пользователя.
Создание хранимых процедур на сервере.
Для примера, можно создать две таблицы на сервере. Первая таблица - Sellers(продавцы), вторая SoldGoods(проданные товары), которые будут связаны между собой по полю Seller.ID - SoldGoods.SellerID
--Создаём таблицу Sellers
CREATE TABLE [dbo].[Sellers] ( [ID] [bigint] IDENTITY (1, 1) NOT FOR REPLICATION NOT NULL, [SellerName] [Nvarchar] (300) COLLATE Cyrillic_General_CI_AS NULL
) ON [PRIMARY]
Sellers - таблица, в которой будем хранить имена продавцов.
Поля:
ID - это уникальное поле-идентификатор с IDENTITY (автоувеличение значения на единицу).
SellerName - поле в котором будет храниться имя продавца.
--Создаём таблицу SoldGoods
CREATE TABLE [dbo].[SoldGoods] ( [ID] [int] IDENTITY (1, 1) NOT FOR REPLICATION NOT NULL, [GoodsName] [Nvarchar] (300) COLLATE Cyrillic_General_CI_AS NULL, [QuantitySold] [Float] NULL, [SoldDate] [datetime] NULL, [SellerID] [bigint] NOT NULL
) ON [PRIMARY]
SoldGoods - таблица в которой будем хранить информацию о проданном товаре
Поля:
ID - это уникальное поле-идентификатор с IDENTITY (автоувеличение значения на единицу).
GoodsName - название проданного товара
QuantitySold - количество проданного товара
SoldDate - дата проданного товара
SellerID - внешний ключ к таблице Sellers. (в котором хранится уникальный номер продавца)
Теперь, для наглядности, заполним обе таблицы выборочными данными.
Сначала таблицу Sellers.
INSERT INTO [Sellers] ([SellerName]) VALUES ('Дмитрий Олегович') INSERT INTO [Sellers] ([SellerName]) VALUES ('Антон Насыров')
INSERT INTO [Sellers] ([SellerName]) VALUES ('Олег Арсеньев') INSERT INTO [Sellers] ([SellerName]) VALUES ('Алексей Логинов')
INSERT INTO [Sellers] ([SellerName]) VALUES ('Альберт Игнатов')
Затем таблицу SoldGoods.
INSERT INTO [SoldGoods] ([GoodsName], [QuantitySold], [SoldDate], [SellerID])
VALUES ('Материнская плата', 5, '20060101', 1) INSERT INTO [SoldGoods] ([GoodsName], [QuantitySold], [SoldDate], [SellerID]) VALUES ('Видеокарта', 16, '20060108', 1)
INSERT INTO [SoldGoods] ([GoodsName], [QuantitySold], [SoldDate], [SellerID]) VALUES ('Монитор', 4, '20060206', 1) INSERT INTO [SoldGoods] ([GoodsName], [QuantitySold], [SoldDate], [SellerID])
VALUES ('Сетевая плата', 8, '20060206', 1) INSERT INTO [SoldGoods] ([GoodsName], [QuantitySold], [SoldDate], [SellerID]) VALUES ('Материнская плата', 6, '20060103', 2)
INSERT INTO [SoldGoods] ([GoodsName], [QuantitySold], [SoldDate], [SellerID]) VALUES ('Монитор', 9, '20060103', 2) INSERT INTO [SoldGoods] ([GoodsName], [QuantitySold], [SoldDate], [SellerID])
VALUES ('Сетевая плата', 14, '20060106', 2) INSERT INTO [SoldGoods] ([GoodsName], [QuantitySold], [SoldDate], [SellerID]) VALUES ('Видеокарта', 7, '20060102', 3)
INSERT INTO [SoldGoods] ([GoodsName], [QuantitySold], [SoldDate], [SellerID]) VALUES ('Материнская плата', 6, '20060109', 3) INSERT INTO [SoldGoods] ([GoodsName], [QuantitySold], [SoldDate], [SellerID])
VALUES ('Монитор', 1, '20060115', 3) INSERT INTO [SoldGoods] ([GoodsName], [QuantitySold], [SoldDate], [SellerID]) VALUES ('Сетевая плата', 30, '20060120', 3)
INSERT INTO [SoldGoods] ([GoodsName], [QuantitySold], [SoldDate], [SellerID]) VALUES ('Видеокарта', 14, '20060106', 4) INSERT INTO [SoldGoods] ([GoodsName], [QuantitySold], [SoldDate], [SellerID])
VALUES ('Материнская плата', 4, '20060106', 4) INSERT INTO [SoldGoods] ([GoodsName], [QuantitySold], [SoldDate], [SellerID]) VALUES ('Монитор', 5, '20060202', 5)
INSERT INTO [SoldGoods] ([GoodsName], [QuantitySold], [SoldDate], [SellerID]) VALUES ('Сетевая плата', 19, '20060105', 5)
С созданием и заполнением таблиц на сервере закончили. Приступим к разработке клиентской части.
Клиентское приложение будет иметь две формы и один DataModule.
Положим на главную (первую) форму список (TCheckListBox), в котором будут выбираться продавцы.
Теперь добавим новую форму (форма для показа отчёта) в проект, и положим на неё сетку (TDBGrid), в которую будут выводиться результаты выборки.
Так же создадим DataModule1: TDataModule и положим на него следующие компоненты:
ADOConnection1: TADOConnection; ADOStoredProc1: TADOStoredProc; DataSource1: TDataSource; ADOStoredProc2: TADOStoredProc; DataSource2: TDataSource; ADOQuery1: TADOQuery; DataSource3: TDataSource;
Получим результат такого вида:
Настройка компонентов: ADOConnection1.LoginPrompt := False; DataSource1.DataSet := ADOStoredProc1; DataSource2.DataSet := ADOStoredProc2; DataSource3.DataSet := ADOQuery1; Form2.DBGrid1.DataSource := DataModule1.DataSource2;
Первая (главная) форма должна использовать (uses) DataModule и Form2
Вторая (форма отчёта) форма должна использовать (uses) DataModule
"Каркас" нашего приложения готов!
Самая первая задача, это соединиться с сервером из нашего приложения. Для этого нам понадобиться файл с расширением ".udl", назовём его "Connect.udl". (Создайте файл в директории с исходным кодом). При запуске этого файла должно появиться окно:
Тут мы и настраиваем соединение с сервером. Затем в обработчике события создания формы напишем код для соединения с сервером:
procedure TForm1.FormCreate(Sender: TObject); begin
//БУДЬТЕ ВНИМАТЕЛЬНЫ, СНАЧАЛА ДОЛЖЕН СОЗДАВАТЬСЯ DataModule1
// закрываем Коннект с базой
DataModule1.ADOConnection1.Close; // указываем файл .udl для ADOConnection1
DataModule1.ADOConnection1.ConnectionString := 'FILE NAME='+GetCurrentDir+'\Connect.udl'; // Указываем провайдера, в данном случае возьмём его из файла .udl
DataModule1.ADOConnection1.Provider := 'FILE NAME='+GetCurrentDir+'\Connect.udl'; // Открываем Коннект
DataModule1.ADOConnection1.Open; end;
Как видим, в коде имеется предупреждение вида: "БУДЬТЕ ВНИМАТЕЛЬНЫ, СНАЧАЛА ДОЛЖЕН СОЗДАВАТЬСЯ DataModule1". Это означает, что в проекте перед созданием главной формы должен создаваться DataModule1. Для этого нужно нажать сочетание клавиш CTRL+SHIFT+F11 и в разделе Auto-Create Forms DataModule1 должен стоять первым. И уберите и списка Form2, эту форму будем создавать динамически.
На данный момент, мы уже имеем процедуру соединения с сервером. (Скомпилируйте и запустите проект, если нет ошибок, продолжаем далее).
Следующая задача, это получение списка продавцов с сервера и заполнения им нашего CheckListBox1, который находится на главной форме. Для этого нам нужно создать хранимую процедуру на сервере, которая будет возвращать нам список, и процедуру в клиентском приложении, которая будет в свою очередь запускать хранимую процедуру и получать данные с сервера.
Начнём с хранимой процедуры на сервере: CREATE PROCEDURE [dbo].[pSelectSellers] AS
--выбираем все из таблицы продавцов
SELECT * FROM SELLERS GO
Затем процедура на клиенте (Все процедуры создаются в главном модуле Form1):
//процедура для получения списка работников procedure TForm1.SelectSellers();
begin
with DataModule1.ADOStoredProc1 do
begin
// закрываем
Close; // присваиваем Connection
Connection := DataModule1.ADOConnection1; // указываем имя хранимой процедуры на сервере
ProcedureName := 'pSelectSellers'; // открываем датасет
Open; // переводим Connection в Nil
Connection := Nil; end;
end;
Таким образом, в дальнейшем мы получим список продавцов в датасет. Приступим к заполнению CheckListBox1.
На событие TForm1.FormShow "вешаем": procedure TForm1.FormShow(Sender: TObject); begin
// запускаем процедуру получения списка работников (см. выше),
// и получаем список продавцов в датасет
SelectSellers(); // очищаем список CheckListBox1
CheckListBox1.Items.Clear; // ставим курсор датасета на первую запись
DataModule1.ADOStoredProc1.First; try
// начало изменений в списке CheckListBox1
CheckListBox1.Items.BeginUpdate; // цикл - пока не достигли конца записей ADOStoredProc1
while not DataModule1.ADOStoredProc1.Eof do
begin
{ заполняем CheckListBox1 именами из таблицы Sellers В параметре AObject : TObject будем хранить значение ID-поля таблицы Sellers}
CheckListBox1.Items.AddObject( DataModule1.ADOStoredProc1.fieldbyname('SellerName').AsString, pointer(DataModule1.ADOStoredProc1.fieldbyname('ID').AsInteger)); // переводим курсор датасета на следующую запись
DataModule1.ADOStoredProc1.Next; Application.ProcessMessages; end; finally
// конец изменений в списке CheckListBox1
CheckListBox1.Items.EndUpdate; end; end;
Мы в цикле заполнили Items.AddObject нашего CheckListBox1, где в первом параметре Const S мы храним имена продавцов, а в AObject храним поле ID, которое будем получать так:
Integer(CheckListBox1.Items.Objects[i]);
На данный момент результат должен быть таков, запускаем проект и получаем
Главная форма: список продавцов.
Наш вариант передачи параметра будет работать по следующему принципу.
Мы получим список продавцов с сервера из таблицы Sellers(имена и идентификаторы).
Затем в клиентском приложении выберем (отметим), нужные имена продавцов,
Создадим временную таблицу на сервере.
В цикле заполним её идентификаторами выбранных продавцов, и будем использовать эту таблицу в запросе на выборку отчёта.
Решения:
Список продавцов получен. (См. выше) Для создания, удаления временной таблицы и выбора отчёта в приложении создадим три отдельные процедуры. Первая процедура из вышеописанных будет "для создания временной таблицы".
{процедура для создания временной таблицы на сервере}
procedure TForm1.CreateTempTable(); begin
with DataModule1.ADOQuery1 do
begin
// закрываем
Close; // присваиваем Connection
Connection := DataModule1.ADOConnection1; // создаём запрос на создание временной таблицы на сервере
SQL.Text := 'CREATE TABLE #TEMP(NUM INT)'; // открываем датасет
ExecSQL; // переводим Connection в Nil
Connection := Nil; end; end;
Тем самым, вызвав эту процедуру, сервер будет создавать временную табличку под названием #TEMP.
Следующая процедура будет "для удаления временной таблицы". {процедура для удаления временной таблицы на сервере}
procedure TForm1.DeleteTempTable(); begin
with DataModule1.ADOQuery1 do
begin
// закрываем
Close; // присваиваем Connection
Connection := DataModule1.ADOConnection1; // создаём запрос на удаление временной таблицы на сервере
SQL.Text := 'DROP TABLE #TEMP'; // открываем датасет
ExecSQL; // переводим Connection в Nil
Connection := Nil; end; end;
И последняя третья процедура на запуск хранимой процедуры на сервере для выбора отчёта.
{процедура для получения отчёта}
procedure TForm1.SelectReport(); begin
with DataModule1.ADOStoredProc2 do
begin
// закрываем
Close; // присваиваем Connection
Connection := DataModule1.ADOConnection1; // указываем имя хранимой процедуры на сервере
ProcedureName := 'pSelectReport'; // обновляем параметры процедуры
Parameters.Refresh; // открываем датасет
Open; // переводим Connection в Nil
Connection := Nil; end; end;
Данная процедура будет запускать на сервере хранимую процедуру под названием pSelectReport, которой у нас пока нет. Создадим её:
CREATE PROCEDURE [dbo].[pSelectReport]
AS
--выбираем данные из таблиц "Продавцы(SELLERS)" и "Проданные товары(SOLDGOODS)"
--при помощи оператора IN в котором будем указывать(выбирать) идентификаторы из таблицы #TEMP
SELECT S.ID AS SELLERID, S.SELLERNAME, SG.GOODSNAME, SG.QUANTITYSOLD, SOLDDATE FROM SELLERS S LEFT JOIN SOLDGOODS SG ON SG.SELLERID = S.ID WHERE S.ID IN (SELECT NUM FROM #TEMP) GO
Примечание: как мы видим, данная процедура использует параметр IN, в котором мы задаём выборку идентификаторов из таблицы #TEMP.
Совет: Так же можно использовать оператор JOIN. Например:
SELECT S.ID AS SELLERID, S.SELLERNAME, SG.GOODSNAME, SG.QUANTITYSOLD, SOLDDATE FROM SELLERS S LEFT JOIN SOLDGOODS SG ON SG.SELLERID = S.ID INNER JOIN #TEMP T ON S.ID = T.NUM
При более сложных запросах, данная конструкция будет более оптимальна.
Итак, мы имеем процедуры на создание и удаление временной таблицы, выбора отчёта, выбора продавцов. Порядок их запуска должен быть примерно таков:
Запускаем процедуру на создание временной таблицы.
Заполняем её в цикле идентификаторами.
Запускаем процедуру на выборку отчёта.
Запускаем процедуру на удаление временной таблицы.
Приступим к написанию основной процедуры в обработчике события нажатия кнопки Button1, которая расположена на Form1.
procedure TForm1.Button1Click(Sender: TObject); var i : integer;
begin
TRY
//начинаем транзакцию
DataModule1.ADOConnection1.BeginTrans; //Запускаем процедуру создающую временную таблицу на сервере (см.выше)
CreateTempTable(); // создаём цикл - до конца записей в списке CheckListBox1
for i := 0 to CheckListBox1.Items.Count-1 do
begin
// если текущий Item в CheckListBox1 находится в состоянии Checked
if CheckListBox1.State[i] = cbChecked then
begin
with DataModule1.ADOQuery1 do
begin
// закрываем
Close; // присваиваем Connection
Connection := DataModule1.ADOConnection1; // создаём запрос на заполнение временной таблицы на сервере
SQL.Text := 'INSERT INTO #TEMP VALUES (:NUM)'; Parameters.ParamByName('NUM').Value := IntToStr(Integer(CheckListBox1.Items.Objects[i])); // открываем датасет
ExecSQL; // переводим Connection в Nil
Connection := Nil; end; CheckListBox1.Selected[i]; end; end; // запускаем процедуру формирования отчёта (см. выше)
SelectReport(); //Удаляем временную таблицу на сервере (см.выше)
DeleteTempTable(); //завершаем транзакцию
DataModule1.ADOConnection1.CommitTrans; //создаём форму отчёта
Application.CreateForm(TForm2, Form2); // показываем её в модальном режиме
Form2.ShowModal; EXCEPT
//при ошибке, откатываем транзакцию
DataModule1.ADOConnection1.RollbackTrans; // показываем диалог ошибки
MessageDlg('Ошибка при формировании отчёта.', mtError, [mbRetry], 0); END; end;
Попробуем "разобрать" данную процедуру.
Сначала мы начали транзакцию и создали временную таблицу на сервере. Затем создали цикл, который "проходит" по всем записям, хранящимся в CheckListBox1, выбирая из параметра AObject уникальный идентификатор каждого отмеченного продавца, и помещаем его в таблицу #TEMP. Далее запускаем процедуру формирования отчёта, в которой, используя, оператор IN, мы задействуем уже существующую и заполненную временную таблицу #TEMP. Выбрав отчёт, мы удаляем временную таблицу, запустив процедуру на её удаление. И последнее. Завершаем транзакцию и показываем форму отчёта.
Примечание: Наша транзакция расположена в блоке TRY : EXCEPT : END; это означает что при возникновении ошибки, она автоматически произведёт откат изменений методом RollbackTrans, и покажет MessageDlg. Иначе транзакция будет успешно завершена методом CommitTrans.
Результат формирования отчёта должен быть таков:
Полученный отчёт.
На этом мы и закончим рассмотрение нашего тестового примера.
К статье прилагается пример с описанием. Для работы примера, необходимо подключить базу (в папке DB) или создать свою, и настроить параметры соединения в файле Connect.udl(в папке Sources)
Проект, используемый в качестве примера (119 K) allGray">
Адресация пользователей в Jabber
Каждый пользователь в сети имеет уникальный идентификатор, адрес — Jabber ID (сокращённо JID). Во избежание необходимости существования сервера с полным списком всех адресов, JID подобно адресу электронной почты содержит имя пользователя (JID node) и DNS-адрес сервера (JID domain), на котором зарегистрирован пользователь, разделённые знаком (@). Например, пользователь user, зарегистрированный на сервере example.com, будет иметь следующий адрес (JID): user@example.com.
Также пользователь может подключаться, находясь в разных местах, сервер позволяет определять дополнительное значение, называемое ресурсом, который идентифицирует клиента пользователя в данный момент. Так можно включить в адрес пользователя (JID) имя его ресурса (JID resource), добавив через слэш в конце адреса.
К примеру, пусть полный адрес пользователя будет user@example.com/work, тогда сообщения, посланные на адрес user@example.com, дойдут на указанный адрес вне зависимости от имени ресурса, но сообщения для user@example.com/work дойдут на указанный адрес только при соответствующем подключённом ресурсе.
Адреса (JID) могут также использоваться без явного указания имени пользователя (с указанием имени ресурса или без такового) для системных сообщений и для контроля специальных возможностей на сервере.
Запомним эту информацию, она нам пригодятся в дальнейшем.
Атрибуты элементов XML
При приходе XML у тегов могут быть следующие основные атрибуты:
to — кому (JID).
From — откуда (JID).
Id — уникальный идентификатор, так называемый атрибут 'системы обнаружения атак'. Позволяет конкретно идентифицировать полученные данные. Рекомендовано делать его случайным. Но в принципе это не обязательно.
xml:lang — текущий язык, кодировка данных.
Version — версия.
Теги могут включать также и дополнительные атрибуты, зависящие от передоваемых данных.
Пример строфы <PRESENCE> с некоторыми атрибутами Вы можете увидеть ниже:
<presence from='delphi-test@jabber.ru/основная' to='delphi-test@jabber.ru/резервная'> <show/>
</presence>
Базовые семантические модули
После того как мы прошли авторизацию, разберем основные базовые семантические модули XML-строф реализованных в протоколе, их довольно немного:
<Presence> — презентационные данные, определяют статусное состояние, видимость пользователей и управление подпиской.
<Message> — собственно сами сообщения переданные или принятые пользователем.
<IQ> — Info Query, данные информационных запросов, включают в себя сами запросы, а также результаты выполнения. Данные запросы позволяют Jabber-клиентам обменивается различными данными между собой. Информационное наполнение запроса и ответа определено в пространстве имен дочернего элемента. Дополнительные расширения протокола (XEP — XMPP Extension Protocol) очень сильно используют <iq> запросы. Подробнее об них я расскажу далее.
Дочерние элементы
У XML-строфы <Message> могут быть следующие дочерние элементы, определенные пространством имен 'jabber:client' или 'jabber:server'. По умолчанию за этими элементами зарезервировано пространство имен 'jabber:client'. Если <Message> имеет тип error то обработка такого сообщения идет в соответствии с RFC 3920 XMPP-Core.
Элемент строфы <Message> может содержать любой из следующих дочерних элементов без явного объявления пространства имен:
<subject/>, <body/>, <thread/>.
Где элемент <body> — является телом сообщения. В значении элемента содержится сам текст сообщения, предварительно перекодированный из-за ограничений XML. Элемент может содержать атрибут 'xml:lang' содержащий язык сообщения. В <Message> могут быть включены множественные экземпляры элементов <Body>, но только при условии, что каждый экземпляр обладает атрибутом 'xml:lang' с отличным языковым значением.
Например:
<message type="chat" to="получатель" id="идентификатор" from="отправитель"> <body>Текст (тело) сообщения</body> </message>
Элемент <Subject> определяет тему сообщения. На него действуют те-же правила, что и для <Body>. Множественные экземпляры <Body> могут быть включены для расширения и дополнения смежных тем, но при условии, что каждый экземпляр обладает атрибутом 'xml:lang' с отличным языковым значением.
Элемент <thread> cлужит для обеспечения хронологии сеанса. Значение <thread> элемента сгенерированного отправителем должно быть послано назад в любых ответах. Этот элемент является дополнительным и обычно не используется для обмена сообщениями между пользователями. Используется он в сеансах связи. Более подробно можно прочитать о нем в RFC 3921.
Дочерние элементы
XML-строфа <Presence> может содержать следующие дочерние элементы, определенные пространством имен 'jabber:client' или 'jabber:server': <show>, <status>, <priority>. По умолчанию за этими элементами зарезервировано пространство имен 'jabber:client'. Если <Message> имеет тип error то обработка такого сообщения идет в соответствии с RFC 3920 XMPP-Core.
Элемент <show> определяет статус контакта и может иметь следующие значения:
away — Отошел,
chat — Готов чатится (В сети),
dnd — Занят,
xa — Недоступен
Пустой элемент <show/> определяет статус контакта " В сети ".
Элемент <status> определяет статусное сообщение. Значением элемента является строка с текстом сообщения, например:
<presence> <status>Смотрю фильм</status> </presence>
Необязательный элемент <priority> определяет приоритет уровня ресурса. Значением элемента является число от -128 до 127.
Информационные запросы <IQ>
Информационные запросы <IQ> разделяются на стандартные, определенные пространством имен 'jabber:client' или 'jabber:server', обеспечивающие базовые функциональные возможности и расширенные.
Расширенные запросы, определенные дополнительными пространствами имен, описаны в различных дополнениях к протоколу XMPP.
Пространство имен расширенных запросов, может содержать любое значение, кроме зарезервированных следующих пространств имен: "jabber:client", "jabber:server" или "http://etherx.jabber.org/streams". Такое расширение позволяет придать протоколу XMPP дополнительную функциональность и гибкость. Таким образом, расширенный информационный запрос <IQ> может содержать один или более дополнительных дочерних элементов, определяющих информационное наполнение, которое расширяет значение данного запроса. Это кардинальное отличие от стандартного информационного запроса.
Стандартный запрос не может содержать, дочерние элементы, кроме элемента <error>. Наличие данного элемента в запросе показывает наличие ошибки.
Поддержка любого расширенного пространства имен является дополнительной возможностью со стороны клиента. Если клиент не понимает такое пространство имен, то есть фактически не поддерживает данное расширение, то он должен проигнорировать данный пакет. Более подробно вы можете прочитать об этом в RFC 3921.
Структурная схема обмена информационными запросами:
Запрос Ответ ---------- ---------- | | | <iq type='get' id='1'> | | ------------------------> | | | | <iq type='result' id='1'> | | <------------------------ | | | | <iq type='set' id='2'> | | ------------------------> | | | | <iq type='error' id='2'> | | <------------------------ | | |
Как мы видим, на структурной схеме обмен между клиентами происходит по такому алгоритму. Запрашивающая сторона посылает <IQ> запрос с атрибутом type равным значению "get". Данный атрибут на принимающей стороне говорит клиенту, что вы должны предоставить информацию по данному запросу (для расширенных запросов при условии, что он поддерживается клиентом). Принимающая сторона отправляет ответ с атрибутом с атрибутом type равным значению "result".
Это первый вариант обмена. Существует и второй, когда запрашивающая сторона информирует принимающую о каком-то изменении, для этого она отправляет запрос с атрибутом type равным значению "set". Данное значение атрибута говорит о том, что принимающая сторона должна обработать присланные данные. Если принимающая сторона не может по каким-либо причинам обработать присланные данные, то в ответ она посылает строфу <IQ> с атрибутом type равным значению "error" информируя запрашивающую сторону о невозможности обработки. Если принимающая сторона корректно обработала запрос c атрибутом "set" то она возвращает ответ с атрибутом равным значению "result".
Пример расширенного запроса определяющий информацию об использованном клиенте (XEP-0092 Software Version):
Запрос:
<iq from='delphi-test2@jabber.ru/QIP' to='delphi-test@jabber.ru/тестовая' xml:lang='ru' type='get' id='qip_30'> <query xmlns='jabber:iq:version'/> </iq>
Ответ:
<iq type='result' to='delphi-test2@jabber.ru/QIP' from='delphi-test@jabber.ru/тестовая' id='qip_30'> <query xmlns='jabber:iq:version'> <name>Мой клиент</name> <version>0.5.0.1</version> </query>
</iq>
Основные сведения о протоколе XMPP
В основе протокола XMPP (eXtensible Messaging and Presence Protocol) лежит язык XML. XMPP является открытым, свободным протоколом для мгновенного обмена сообщениями и информацией о присутствии в режиме околореального времени.
Изначально спроектированный легко расширяемым протокол помимо передачи текстовых сообщений поддерживает передачу голоса и файлов по сети.
Данный протокол принят как стандарт RFC.
Стандартный порт для Jabber-клиентов — 5222.
Протокол регламентируется следующими стандартами:
RFC 3920 — Extensible Messaging and Presence Protocol (XMPP): Core
RFC 3921 — Extensible Messaging and Presence Protocol (XMPP): Instant Messaging and Presence
Следует также отметить, так как протокол является текстовым, а не бинарным соответственно у этого протокола есть слабые стороны, а именно: избыточность передаваемой информации, отсутствие возможности передачи двоичных данных приводит к использованию различных способов перекодировки. В результате этого, для передачи файлов приходится использовать дополнительные протоколы, например HTTP. Если этого не избежать, то XMPP обеспечивает встроенную передачу файлов кодируя информацию используя base64. Другая двоичная информация, такая как закодированный разговор или графические иконки включаются с использованием такого же метода. Однако прежде чем двигаться дальше рассмотрим адресацию пользователей с Jabber-сетях.
Отправка и прием сообщений <Message>
Прием и отправка сообщений осуществляется через XML-строфу <MESSAGE>. Так как в Jabber-е предусмотрены разные типы сообщений, то для их разграничений предусмотрен атрибут type содержащий информацию о типе сообщения.
Типы сообщений могут быть следующие:
chat — Одиночное сообщение от клиента к клиенту.
error — Сообщение об ошибке. Произошедшая ошибка связанна с предыдущим, посланным одиночным сообщением.
groupchat — Групповой чат. Данное сообщение пришло с группового чата, действующего по признаку "Одно сообщение — многим получателям".
headline — Системное сообщение, автоматически генерируется различными сервисами для шировещательной рассылки (новости, спорт, RSS-каналы и пр.) Отвечать на такие сообщение не нужно, да и не зачем.
normal — одиночное сообщение, посылаемое вне контекста взаимно-однозначного сеанса связи или группового чата. То есть это такое сообщение, на которое пользователь может дать ответ, не учитывая хронологии сеанса связи.
Перекодировка символов текста
Поскольку символы "<" и ">" используются для обозначения самих XML тегов, то их вставка в текст сообщения недопустима (за исключением случая, когда вставлен символ ">", но никакой тег не был открыт). Поэтому для корректного формирования XML следующие символы должны быть заменены в теле сообщения при отправке оного и соответственно обратно возвращены при приеме:
"<" в "<"
">" в ">".
Таким образом, чтобы написать "2>1", нужно написать "2>1". То же самое касается и знака "&" — он заменяется "&". Также рекомендуется заменять и кавычки (хотя в большинстве случаев они хорошо распознаются и без этого). Эквивалент двойных кавычек — """
Подготовка
Сразу оговорюсь, что я не ставлю перед собой задачу написать полноценно работающий клиент соответствующий полному стандарту XMPP. Слишком большой труд, скажем так, однако основные методы работы с XMPP будут включены в мой исходный компонент.
В качестве основы для работы клиента мной были взяты наработки по работе с WinSock , используемые им в TICQClient, немного портированные, кое-где измененные и дополнительно комментированные мной, для нашего демо-клиента.
В качестве парсера XML мной был взят TjanXMLParser2, благо он бесплатный, довольно быстрый. Стандартный парсер MSXML был мной отброшен по причине, того, что некоторые XML-пакеты приходили синтаксически неправильные, что начисто отрубало желание этого парсера работать с ними.
Что касается приведенных далее листингов обмена протоколом, я постарался описать самые интересные части, если у вас кое-где возникнут вопросы, подробнее вы можете узнать в RFC. Все 800 основного RFC страниц я не смогу Вам подробно изложить, но критические места постараюсь.
Также сразу оговорюсь, что наш пример не будет поддерживать шифрование, то есть данные будут передаваться в открытом виде. Сделано это для упрощения понимания примера. Вышло, то, что вышло, а хорошо иль плохо получилось судить Вам, уважаемые коллеги.
Итак, для тестирования нашего примера, мной был зарегистрирован на сайте jabber.ru аккаунт delphi-test@jabber.ru с паролем delphi-test. Эти данные нам понадобятся для разбора протокола обмена между сервером jabber (далее — Сервер) и нашим клиентом (также — Клиент) далее.
Прохождение аутенфикации
Итак, первым действием при соединении с сервером Jabber, которым должен выполнить наш клиент — является аутенфикация. Аутенфикация будет происходить используя механизм SASL аутенфикации, описанный в в "RFC 2831 — Using Digest Authentication as a SASL Mechanism", алгоритм работы который будет рассмотрен подробнее, чуть далее.
Итак, мы установили физическое соединение с сервером, теперь нам нужно пройти аутенфикацию, для этого клиент посылает серверу следующий пакет:
<?xml version='1.0' encoding='UTF-8'?> <stream:stream to='jabber.ru' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' xml:l='ru' version='1.0'>
В ответ сервер пришлет подтверждение, о рукопожатии:
<?xml version='1.0'?> <stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' id='3966489307' from='jabber.ru' version='1.0' xml:lang='en'>
Сразу же после приема первого пакета, придет пакет, содержащий информацию о возможностях и доступных механизмах сервера. Данные возможности нужны, будут для полноценной работы с сервером:
<stream:features><starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/> <compression xmlns='http://jabber.org/features/compress'>
<method>zlib</method> </compression> <mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'> <mechanism>DIGEST-MD5</mechanism> <mechanism>PLAIN</mechanism></mechanisms>
<register xmlns='http://jabber.org/features/iq-register'/> </stream:features>
Что мы видим в пакете, видим, что сервер поддерживает zip компрессию при передаче пакетов, поддерживает механизм аутенфикации DIGEST-MD5, и другие возможности. Стоит также отметить, что возможности сервера зависят от самого сервера и в зависимости от программы могут изменяться. Подробнее вы можете узнать в RFC 3920. Однако нас интересует то, что сервер поддерживает механизм аутенфикации DIGEST-MD5. Отлично, скажем мы и отправим ему пакет, говорящий о том, что мы хотим пройти аутенфикацию используя механизм DIGEST-MD5.
<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='DIGEST-MD5'/>
После получения данного пакета сервер присылает нам, так называемый challenge-пакет:
<challenge xmlns='urn:ietf:params:xml:ns:xmpp-sasl'> bm9uY2U9IjIyNjQ3NzQ4Iixxb3A9ImF1dGgiLGNoYXJzZXQ9dXRmLTgsYWxnb3JpdGhtPW1kNS1zZXNz </challenge>
Данный пакет мы должны будем разобрать. Как это сделать? Ранее внимательный читатель обратил внимание, что некоторые пакеты могут передаваться в кодировке Base64. Это наш случай. Текстовый элемент содержит информацию в данной кодировке, которая после раскодирования примет следующий вид:
nonce="22647748",qop="auth",charset=utf-8,algorithm=md5-sess
Из этой строки нам понадобится значение Nonce для последующего построения ответа серверу, после чего мы подготавливаем строку ответа, которую мы передадим на сервер в ответном пакете, предварительно закодировав ее в Base64. Итак, ответная строка будет иметь следующий вид:
username="delphi-test", realm="jabber.ru", nonce="22647748", cnonce="2313e069649daa0ca2b76363525059ebd", nc=00000001, qop=auth, digest-uri="xmpp/jabber.ru" ,charset=utf-8, response=16351f86cc5591312e20b4ccd880eadb
где:
username — JID-node пользователя
realm — JID-domain пользователя
nonce — Уникальный код сессии, присланный нам ранее сервером
cnonce — Уникальный код ответной клиентской сессии, сгенерированный клиентом
nc — Так называемый once count — сколько раз был использован текущий nonce. Обычно значение параметра равно 00000001, его и будем использовать. На самом деле параметр довольно интересный и стоит отдельного рассмотрения и изучения в RFC, но как показала практика его смело можно игнорировать.
digest-uri — Протокол подключения, для XMPP сервера он состоит из соединения строк "xmpp/" + JID Domain
charset — поддержка кодировки пароля и имени, в нашем случае UTF-8
И самый важный параметр response в котором заключен ключ ответа серверу, включающий в себя пароль и ответные данные в формате MD5 строящийся по определенному алгоритму.
Алгоритм построения строки ответа и параметра Response более подробно мы рассмотрим далее в подразделе "RFC 2831 использование MD5-Digest аутенфикации в SASL". Пока примем к сведению, что текущее и следующие два действие относится уже к данному алгоритму.
Итак, строку ответа, мы сформировали, закодировали в Base64 и отправляем обратно серверу (всё это должно быть в одну строчку, но, чтобы страница не расползалась, разбито на несколько):
<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'> dXNlcm5hbWU9ImRlbHBoaS10ZXN0IixyZWFsbT0iamFiYmVyLnJ1Iixub25jZT0iMjI2ND c3NDgiLGNub25jZT0iMjMxM2UwNjk2NDlkYWEwY2EyYjc2MzYzNTI1MDU5ZWJkIixu Yz0wMDAwMDAwMSxxb3A9YXV0aCxkaWdlc3QtdXJpPSJ4bXBwL2phYmJlci5ydSIsY2 hhcnNldD11dGYtOCxyZXNwb25zZT0xNjM1MWY4NmNjNTU5MTMxMmUyMGI0Y2Nk ODgwZWFkYg== </response>
Если все нормально мы получим следующий ответ:
<challenge xmlns='urn:ietf:params:xml:ns:xmpp-sasl'> cnNwYXV0aD1lOTg5NjZjZjUxNjliZWUzOTYzNGU5Zjk5ZTIzZDZhYg== </challenge>
Тут нам особо ничего не нужно, подтверждаем принятие его, отправив со стороны клиента:
<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>
И если все прошло успешно, то получаем со стороны сервера пакет, говорящий нам о том, что аутенфикация прошла успешно:
<success xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>
Далее мы снова посылаем пакет рукопожатия:
<?xml version='1.0' encoding='UTF-8'?> <stream:stream to='jabber.ru' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' xml:l='ru' version='1.0'>
Получаем ответ:
<?xml version='1.0'?> <stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' id='4096919146' from='jabber.ru' version='1.0' xml:lang='en'>
После чего по стандарту мы должны связать нашего клиента с JID-ресурсом, что мы и делаем, посылая строку в формате UTF-8:
<iq type='set' id='bund_2'> <bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'> <resource>тестовая</resource>
</bind> </iq>
Примечание: Все листинги будут представлены в ASCII формате, хотя на самом деле прием и посылка пакетов ведется в UTF-8. Однако что бы Вам не читать крякозаблы в листингах примеров, кодировка будет в показана в ASCII.
Сервер подтверждает связывание ресурса с данным клиентом:
<iq id='bund_2' type='result'> <bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>
<jid>delphi-test@jabber.ru/тестовая</jid> </bind> </iq>
Клиент посылает пакет присутствия в сети
<presence><show></show></presence>
и все, аутенфикация пройдена, значок клиента в контакт-листе становится зелененьким, теперь он может посылать и принимать сообщения.
Пространства имен XML
Так как первоначально XMPP был задуман, как протокол, поддерживающий расширения, перед разработчиками встал вопрос, как можно реализовать данные расширения, не внося коррективы в основной протокол. И решение нашлось. Это решение — пространство имен, довольно известное в XML.
Пространство имён в XML — именованная совокупность имён элементов и атрибутов, служащая для обеспечения их уникальности в XML-документе. Все имена элементов в пределах пространства имён должны быть уникальны. Таким образом, реализуется различение одинаковых элементов XML или атрибутов. Для клиентов Jabber зарезервировано пространство имен "jabber:client"
Пространства имён объявляются с помощью зарезервированного XML атрибута xmlns, значение которого является названием пространства имен.
Например, элемент <QUERY> описанный пространством имен 'jabber:iq:roster' выглядит так:
<query xmlns='jabber:iq:roster'>
Работа с ростер-листом (списком контактов)
Ростер-лист или аналог списка контактов в сетях ICQ в Jabber-е представлен списком, содержащим JID-контакты в виде элементов XML хранящимся на сервере от имени пользователя. Так как ростер-лист сохранен сервером от имени пользователя, то пользователь может обратиться к информации списка от любого ресурса.
Управление ростер-листом (списком) осуществляется через расширенный информационный запрос <IQ> содержащий дочерний элемент <query> c пространством имен 'jabber:iq:roster'. Элемент <query> может содержать один или более дочерних элементов <ITEM> содержащих информацию о контакте.
Уникальный идентификатор каждого элемента списка <item> — это JID контанта, формируемый в атрибуте jid Значение атрибута jid имеет форму user@domain без указания ресурса. Текущее состояние подписки пользователя (контакта) относительно элемента <item> зафиксировано в атрибуте subscription и может принимать следующие значения:
none — У пользователя нет подписки к контакту, нет подписки и к информации присутствия пользователя
to — у пользователя есть подписка к информации присутствия контакта, но у контакта нет подписки к информации присутствия пользователя
from — у контакта есть подписка к информации присутствия пользователя, но у пользователя нет подписки к информации присутствия контакта
both — у пользователя есть подписка к присутствию контакта, да и у контакта есть подписка к пользователю.
RFC 2831 использование MD5-Digest аутенфикации в SASL
Итак, аутенфикация решает следующие задачи: Передача пароля на сервер, в закрытом виде, защиту от повторяющихся атак (monitoring nc value), защиту (monitoring nonce) в определённый промежуток времени от определённого клиента. Для того, что бы понять, как работает данный стандарт, разберем основы SASL.
Общие принципы работы SASL
Метод SASL (Simple Authentication and Security Layer) используется для добавления поддержки аутентификации в различные протоколы соединения. Для аутентификации могут быть использованы различные механизмы.
Имя требуемого механизма задаётся клиентом в команде аутентификации. Если сервер поддерживает указанный механизм, он посылает клиенту последовательность окликов (challenges), на которые клиент посылает ответы (responses), чтобы удостоверить себя. Содержимое окликов и ответов определяется используемым механизмом и может представлять собой двоичные последовательности произвольной длины. Кодировка последовательностей определяется прикладным протоколом. Вместо очередного оклика сервер может послать подтверждение аутентификации или отказ. Кодировка также определяется протоколом. Вместо ответа клиент может послать отказ от аутентификации. Кодировка опять определяется протоколом. В результате обменов откликам и ответами должна произойти аутентификация (или отказ), передача идентификатора клиента (пустой идентификатор влечёт получение идентификатора из аутентификации) серверу и, возможно, договорённость об использовании безопасного транспортного протокола (security layer), включая максимальный размер буфера шифрования.
Идентификатор клиента может отличаться от идентификатора, определяемого из аутентификации, для обеспечения возможности работы прокси.
Реализация на примере механизма MD5-Digest
Схема работы SASL для нашего клиента основана на использовании механизма MD-Digest и имеет следующий алгоритм работы:
Сервер посылает случайную строку nonce, наличие поддержки utf-8 в параметре charset для имени и пароля, алгоритм аутентификации (обязательно md5-sess) в параметре algorithm.
То есть те данные, что мы раскодировали ранее из пакета challenge:
nonce="22647748",qop="auth",charset=utf-8,algorithm=md5-sess
Клиент отвечает строкой, содержащей: идентификатор клиента username, идентификатор домена realm, полученную от сервера случайную строку nonce, случайную строку клиента cnonce, номер запроса (позволяет серверу заметить попытку replay attack) nc. параметр digest-uri (сочетание имени сервиса, имени сервера т.е. 'xmpp/' + JID Domain), строку responce подтверждающею знание пароля и ответ на оклик (MD5 от имени пользователя, realm, пароля, случайной строки сервера, случайной строки клиента, идентификатора клиента, номера запроса, уровня защиты, digest-uri; некоторые компоненты берутся в виде MD5, некоторые в исходном виде, некоторые в обоих видах), использование utf-8 для имени и пароля, принятый алгоритм шифрования и идентификатор клиента.
То есть, как вы догадались эта та строка, которую мы формируем в ответ:
username="delphi-test", realm="jabber.ru", nonce="22647748", cnonce="2313e069649daa0ca2b76363525059ebd", nc=00000001, qop=auth, digest-uri="xmpp/jabber.ru", charset=utf-8, response=16351f86cc5591312e20b4ccd880eadb
Сервер проверяет ответ на оклик и посылает ответ на ответ в похожем формате (но всё же отличающемся, чтобы клиент мог убедиться в подлинности сервера). Данный механизм слабее системы с открытыми ключами, но лучше простой CRAM-MD5.
Примечание: Стоит отметить, что может предусматриваться упрощённый протокол повторной аутентификации (начинается сразу с посылки клиентом ответа с увеличенным на 1 номером запроса).
Алгоритм вычисления строки ответа response
Алгоритм вычисления строки ответа response имеет следующую формулу:
response-value = HEX( KD ( HEX(H(A1)), { nonce-value, ":" nc-value, ":", cnonce-value, ":", qop-value, ":", HEX(H(A2)) })) A1 = { H( { username-value, ":", realm-value, ":", passwd } ), ":", nonce-value, ":", cnonce-value } A2 = { "AUTHENTICATE:", digest-uri-value }
Где:
Выражение { a, b, ... } — означает сложение строк a, b
HEX(n) — 16-байтовый MD5-хеш n, приведенный в 32 байтовую Hex-строку в нижнем регистре. Фактически строковое представление дайджеста MD5.
H(s) — 16-байтовый MD5-хеш строки s
KD(k, s) — объединение данных (строк) k, s
H({k, ":", s}) — 16-байтовый MD5-хеш, полученный в результате сложения строки k, ":", S
Как видите, особо ничего сложного нет. Вот алгоритм расчета реализованный мной на Delphi:
function GenResponse(UserName, realm, digest_uri, Pass, nonce, cnonce : String) : string;
const nc = '00000001'; gop = 'auth'; var A2, HA1, HA2, sJID : String; Razdel : Byte; Context : TMD5Context; DigestJID : TMD5Digest; DigestHA1 : TMD5Digest; DigestHA2 : TMD5Digest; DigestResponse : TMD5Digest; begin Razdel := Ord(':'); // ВЫЧИСЛЯЕМ А1 по формуле RFC 2831
// A1 = { H( { username-value, ":", realm-value, ":", passwd } ), // ":", nonce-value, ":", cnonce-value, ":", authzid-value } sJID := format('%S:%S:%S', [username, realm, Pass]); MD5Init(Context); MD5UpdateBuffer(Context, PByteArray(@sJID[1]) , Length(sJID)); MD5Final(DigestJID, Context);
MD5Init(Context); MD5UpdateBuffer(Context, PByteArray(@DigestJID),SizeOf(TMD5Digest)); MD5UpdateBuffer(Context, @Razdel , SizeOf(Razdel)); MD5UpdateBuffer(Context, PByteArray(@nonce[1]) , Length(nonce)); MD5UpdateBuffer(Context, @Razdel , SizeOf(Razdel)); MD5UpdateBuffer(Context, PByteArray(@cnonce[1]) , Length(cnonce)); MD5Final(DigestHA1, Context);
// ВЫЧИСЛЯЕМ А2 по формуле RFC 2831
// A2 = { "AUTHENTICATE:", digest-uri-value } A2 := format('AUTHENTICATE:%S', [digest_uri]); MD5Init(Context); MD5UpdateBuffer(Context, PByteArray(@A2[1]) , Length(A2)); MD5Final(DigestHA2, Context);
// ВЫЧИСЛЯЕМ RESPONSE по формуле RFC 2831 // HEX( KD ( HEX(H(A1)), // { nonce-value, ":" nc-value, ":",
// cnonce-value, ":", qop-value, ":", HEX(H(A2)) })) HA1 := LowerCase( PacketToHex(@DigestHA1, SizeOf(TMD5Digest))); HA2 := LowerCase( PacketToHex(@DigestHA2, SizeOf(TMD5Digest))); MD5Init(Context); MD5UpdateBuffer(Context, PByteArray(@HA1[1]),Length(HA1)); MD5UpdateBuffer(Context, @Razdel , SizeOf(Razdel)); MD5UpdateBuffer(Context, PByteArray(@nonce[1]) , Length(nonce)); MD5UpdateBuffer(Context, @Razdel , SizeOf(Razdel)); MD5UpdateBuffer(Context, PByteArray(@nc[1]) , Length(nc)); MD5UpdateBuffer(Context, @Razdel , SizeOf(Razdel)); MD5UpdateBuffer(Context, PByteArray(@cnonce[1]) , Length(cnonce)); MD5UpdateBuffer(Context, @Razdel , SizeOf(Razdel)); MD5UpdateBuffer(Context, PByteArray(@gop[1]) , Length(gop)); MD5UpdateBuffer(Context, @Razdel , SizeOf(Razdel)); MD5UpdateBuffer(Context, PByteArray(@HA2[1]),Length(HA2)); MD5Final(DigestResponse, Context); Result := LowerCase( PacketToHex(@DigestResponse, SizeOf(TMD5Digest)) ) end;
На входе функция получает параметры рассмотренные нами ранее.
Статусы, состояния, информация о присутствии, управление подпиской <Presence>
Прием и отправка статусных сообщений, а также информации о видимости контактов и подписки на сообщения от них, осуществляется через XML-строфу <Presence>.
Атрибут type строфы <Presence> является дополнительным.
Строфа, которая не обладает атрибутом type, используется Jabber-ом, для сообщений о присутствии контакта в сети Jabber и указывает на то, что данный контакт находится в сети (онлайне) и доступен для коммуникации.
Если атрибут type присутствет в строфе <Presence>, то он управляет подпиской на сообщения и смену статусов другого контакта (объекта). Аналог подписки в IM-сетях является прохождение авторизации в ICQ.
Если атрибут включен, то он должен содержать иметь одно из следующих значений:,
unavailable — Сигнализирует, что данный контакт, больше не доступен для коммуникаций. Фактически контакт вышел в оффлайн.
subscribe — Запрос на подписку (авторизацию) от другого контакта.
subscribed — Информирует о том, что контакт разрешил авторизацию.
unsubscribe — Отправитель аннулирует подписку.
unsubscribed — Запрос на аннулирование подписки (отозвание авторизации) от другого контакта.
probe — Запрос о текущем присутствии контакта только сервером от имени пользователя.
error — Ошибка, произошедшая при доставки предыдущих данных. Обработка такого сообщения идет в соответствии с RFC 3920 XMPP-Core.
Например, запрос на подписку от контакта ivanov@jabber.ru для нашего контакта может выглядеть так:
<presence to="delphi-test@jabber.ru" type="subscribe" from="ivanov@jabber.ru"/> </presence>
Разрешение авторизации в ответ :
<presence to="ivanov@jabber.ru" type="subscribed"/>
А запрет (отказ) вот так:
<presence to="ivanov@jabber.ru" type="unsubscribed"/>
Структура XML-пакетов Jabber протокола (XML Streams)
Структура XML пакетов получаемых с сервера и передаваемых на него по спецификации RFC 3920 имеет следующий вид:
|--------------------| | <stream> | |--------------------| | <presence> | | <show/> | | </presence> | |--------------------| | <message to='foo'> | | <body/> | | </message> | |--------------------| | <iq to='bar'> | | <query/> | | </iq> | |--------------------| | ... | |--------------------| | </stream> | |--------------------|
Как вы видите, на схеме представлена иерархическая структура XML подразделенная на следующие элементы, так называемый поток XML и элементы строф XML.
Поток XML — является контейнером для хранения элементов строф XML. Поток XML начинается с открытия тэга <STREAM> (с соответствующими атрибутами и пространством имен), конец потока XML заканчивается закрытием тега </STREAM>. Во время обмена с сервером, клиент и сам сервер может посылать неограниченное количество элементов строф в потоке XML.
Строфы XML — это дискретные семантические модули представленные элементами, заключенными в потоке XML. Строфы XML являются дочерними элементами (child node) корня XML <STREAM>. Начало любой строфы XML обозначено началом элемента (например, <PRESENCE>), конец строфы XML обозначен завершающим тегом (</PRESENCE>). В примере строфы XML: <PRESENCE>, <MESSAGE>, <IQ>. Каждая строфа XML представляет собой конкретную информацию, так например строфа <MESSAGE> представляет сообщение, а <IQ> информационный запрос. Более подробно строфы будут рассмотрены далее.
Примечание: несмотря на стандарт, мной было замечено, что с некоторых серверов могут приходить пакеты, просто содержащие строфы XML, но включенные в поток XML.
Управление ростер-листом
Добавление или редактирование контакта. При отсутствии контакта в ростер-листе контакт будет добавлен, при наличии отредактирован.
Добавление / корректировка. Клиент посылает следующий пакет.
<iq from='delphi-test@jabber.ru/тестовая' type='set' id='уникальный номер'> <query xmlns='jabber:iq:roster'> <item jid='новый/корректируемый JID' name='Имя контакта'> <group>Группа контакта</group>
</item> </query> </iq>
После добавления/обновления информации о контакте на сервере, сервер оповещает все доступные ресурсы пользователя о внесенной информации. Оповещение служит сигналом для синхронизации данных клиентов о данном контакте с данными сервера.
Оповещение сервера:
<iq to='delphi-test@jabber.ru/тестовая' type='set' id='уникальный номер'>
<query xmlns='jabber:iq:roster'> <item jid='новый/корректируемый JID' name='Имя контакта'> <group>Группа контакта</group> </item> </query>
</iq>
Информация о результате:
<iq to='delphi-test@jabber.ru/тестовая' type='result' id='уникальный номер'/>
В любое время, пользователь удалить контакт из ростер-списка, для этого клиент должен послать запрос с атрибутом subscription элемента <item> равным значению 'remove':
<iq from=' delphi-test@jabber.ru/тестовая' type='set' id='roster_4'> <query xmlns='jabber:iq:roster'> <item jid='JID который удаляется' subscription='remove'/> </query> </iq>
Как и в случае с добавлением/корректировкой контакта сервер оповещает клиенты о удалении контакта. Указанием факта удаления служит атрибут subscription равным значению 'remove' в элементе <item>.
<iq to=' delphi-test@jabber.ru/тестовая' type='set' id='roster_4'> <query xmlns='jabber:iq:roster'> <item jid='JID который удален сервером' subscription='remove'/> </query> </iq>
система для быстрого обмена сообщениями
Jabber — система для быстрого обмена сообщениями и информацией о присутствии (в контакт-листе) между любыми двумя пользователями Интернета на основе открытого протокола XMPP.
В отличии от той же Аськи Jabber-сеть имеет на мой взгляд более развитые возможности, а наличие расширений протокола открывает горизонты функциональности на недосягаемые для коммерческих IM-сетей, вот некоторые из них:
Открытость: протокол Jabber открыт, общедоступен и достаточно лёгок для понимания; существует множество реализаций серверов и клиентов, а также библиотек с открытым исходным кодом.
Расширяемость: с помощью пространств имён в XML можно расширить протокол Jabber для выполнения требуемых задач и для обеспечения поддержки взаимодействия между различными системами. Общие расширения разрабатываются под контролем Jabber Software Foundation.
Децентрализованность: кто угодно может запустить свой собственный сервер Jabber, что позволяет организациям и частным лицам заниматься любыми экспериментами с IM.
Безопасность: любой сервер Jabber может быть изолирован от общедоступной сети Jabber, многие из вариантов реализации сервера используют SSL при обмене между клиентом и сервером, и немало[источник не указан 39 дней] клиентов поддерживают шифрование с помощью PGP/GPG внутри протокола.
Jabber удовлетворяет многие потребности частных лиц и организаций. Но важно понимать, что он не является универсальным решением всех задач. В частности, Jabber не является:
Универсальным чат-клиентом для различных систем IM — несмотря на множество клиентов Jabber под различные платформы, они не предоставляют таких возможностей по взаимодействию с различными системами IM, которые обеспечиваются программами Miranda IM, Trillian или Pidgin: вместо этого взаимодействие между Jabber и другими системами осуществляют шлюзы, расположенные на стороне сервера.
Универсальным решением проблем взаимодействия с различными IM-системами — некоторые сервера Jabber предоставляют возможность взаимодействия с другими системами IM через шлюзы, которые транслируют протокол Jabber в протокол этих систем; однако только от самих систем зависит осуществление взаимодействия (к чему они подчас не стремятся, и даже наоборот).
Как вы видите, ничего особо
Как вы видите, ничего особо сложного нет. Простой Jabber-клиент с минимальной функциональностью представлен в примере. Также в архив выложен парсер TjanXMLParser2, RFC 3920, 3921.
К статье прилагается .
См. обсуждение статьи на сайте delphikingdom.com.
Запрос списка контактов при входе в систему
При входе в систему клиент Jabber должен послать серверу информационный запрос о получении ростер-листа.
Запрос ростер-листа клиентом:
<iq from='delphi-test@jabber.ru/тестовая' type='get' id='roster_1'> <query xmlns='jabber:iq:roster'/> </iq>
Получение ростер-листа с сервера:
<iq from='delphi-test@jabber.ru' to='delphi-test@jabber.ru/тестовая' id='roster_1' type='result'> <query xmlns='jabber:iq:roster'> <item subscription='from' name='Тест 2' jid='delphi-test2@jabber.ru'/> </query> </iq>