Delphi - сбориник статей

         

List


//Для всех частей тела for i := 0 to numBodyParts -1 do begin //Начать поиск ключевых кадров KeyFrameIndex:=-1; for j := 0 to BodyParts[i, CurrentActions[i]].KeyFrameCount-1 do if Timers[i]=BodyParts[i, CurrentActions[i]].KeyFrames[j].KFTimer then KeyFrameIndex:=j; if keyFrameIndex=-1 then begin //Допустим, что кадр не найден MaxLower:=0; MinUpper:=BodyParts[i, CurrentActions[i]].Duration+1; for j := 0 to BodyParts[i, CurrentActions[i]].KeyFrameCount-1 do begin if BodyParts[i, CurrentActions[i]].KeyFrames[j].KFTimer>Timers[i] then begin if Bodyparts[i, CurrentActions[i]].KeyFrames[j].KFTimer<MinUpper then MinUpper:=Bodyparts[i, CurrentActions[i]].KeyFrames[j].KFTimer; end else if BodyParts[i, CurrentActions[i]].KeyFrames[j].KFTimer>MaxLower then MaxLower:=BodyParts[i, CurrentActions[i]].KeyFrames[j].KFTimer; end; //Взять значения углов поворота из этих кадров for j := 0 to BodyParts[i, CurrentActions[i]].KeyFrames[0].BoneCount-1 do begin for k:= 0 to Bodyparts[i, CurrentActions[i]].KeyframeCount-1 do begin if MaxLower=BodyParts[i, CurrentActions[i]].KeyFrames[k].KFTimer then begin Anglex1:=Bodyparts[i, CurrentActions[i]].keyFrames[k].BoneState[j].AngleYOZ; Angley1:=Bodyparts[i, CurrentActions[i]].keyFrames[k].BoneState[j].AngleXOZ; Anglez1:=Bodyparts[i, CurrentActions[i]].keyFrames[k].BoneState[j].AngleXOY; end; if MinUpper=BodyParts[i, CurrentActions[i]].KeyFrames[k].KFTimer then begin Anglex2:=Bodyparts[i, CurrentActions[i]].keyFrames[k].BoneState[j].AngleYOZ; Angley2:=Bodyparts[i, CurrentActions[i]].keyFrames[k].BoneState[j].AngleXOZ; Anglez2:=Bodyparts[i, CurrentActions[i]].keyFrames[k].BoneState[j].AngleXOY; end; end; //Вычислить значения углов поворота в данный момент времени anglex:=(Anglex2-anglex1)/(minUpper-maxLower)*Timers[i]+anglex1- (Anglex2-anglex1)/(minUpper-maxLower)*maxlower; angley:=(Angley2-angley1)/(minUpper-maxLower)*Timers[i]+angley1- (Angley2-angley1)/(minUpper-maxLower)*maxlower; anglez:=(Anglez2-anglez1)/(minUpper-maxLower)*Timers[i]+anglez1- (Anglez2-anglez1)/(minUpper-maxLower)*maxlower; //Записать эти значения в массив подобъекта Body Body.DeformationBoneState[Bodyparts[i, CurrentActions[i]].KeyFrames[0].BoneIndexes[j]].AngleYOZ:=anglex; Body.DeformationBoneState[Bodyparts[i, CurrentActions[i]].KeyFrames[0].BoneIndexes[j]].AngleXOZ:=angley; Body.DeformationBoneState[Bodyparts[i, CurrentActions[i]].KeyFrames[0].BoneIndexes[j]].AngleXOY:=anglez; end; end else begin //Если же ключевой кадр найден, просто переписать из него значения for j := 0 to BodyParts[i, CurrentActions[i]].KeyFrames[KeyFrameIndex].BoneCount-1 do begin Body.DeformationBoneState[Bodyparts[i, CurrentActions[i]].KeyFrames[KeyFrameIndex].BoneIndexes[j]].AngleYOZ:= Bodyparts[i, CurrentActions[i]].KeyFrames[KeyFrameIndex].BoneState[j].AngleYOZ; Body.DeformationBoneState[Bodyparts[i, CurrentActions[i]].KeyFrames[KeyFrameIndex].BoneIndexes[j]].AngleXOZ:= Bodyparts[i, CurrentActions[i]].KeyFrames[KeyFrameIndex].BoneState[j].AngleXOZ; Body.DeformationBoneState[Bodyparts[i, CurrentActions[i]].KeyFrames[KeyFrameIndex].BoneIndexes[j]].AngleXOY:= Bodyparts[i, CurrentActions[i]].KeyFrames[KeyFrameIndex].BoneState[j].AngleXOY; end; end; end; end;



Таблица функций




Синтаксис Альтернативный синтаксис Описание
1 ABS(X) |X| Модуль
2 COS(X) Косинус
3 EXP(X) Экспонента
4 LN(X) Натуральный логарифм
5 LOG10(X) Десятичный логарифм
6 LOG2(X) Двоичный логарифм.
7 LOGN(n, X) Произвольный логарифм по основанию n
8 MAX(X1, X2,..., XN) Максимум из перечисленных выражений
9 MIN(X1, X2,..., XN) Минимум из перечисленных выражений
10 N(m, ?) Генератор случайных чисел по нормальному закону с заданным математическим (m) ожиданием и средне квадратичным отклонением (?)
11 POWER(X , Y) X^Y Возведение X в степень Y
12 PROD(ixFrom,ixTo,Xi)
PROD(Xi)
Произведение выражений Xii:= ixFrom, ixTo ixFrom = <Целое число> ixTo = <Целое число> | Dim [ - <Целое число>] Если ixFrom и ixTo не заданны, то они полагаются: ixFrom = 1 и ixTo = Dim На ixFrom и ixTo налагаются следующие условия:( ixFrom >=1) и (ixFrom < ixTo) и (ixTo <= Dim)
13 R(A, B) Генератор случайных чисел по равномерному закону в диапазоне от A до B.
14 ROOT(X, Y) Извлечение корня степени Y из выражения X
15 SIGN(X) Знак выражения a. Возможные значения {-1, 0, 1}
16 SIN(X) Синус
17 SQR(X) Возведение в квадрат
18 SQRT(X) Извлечение квадратного корня
19 SUM(ixFrom,ixTo,Xi)
SUM(Xi)
Сумма выражений Xi, см. описание для PROD
20 TAN(X) Тангенс



Формат файла модели


Этот формат и необходимо определить перед тем, как писать процедуру загрузки. Во-первых, файл должен содержать знак - признак файла модели. В качестве такого знака можно использовать три буквы, которые одновременно будут и расширением файла.

Поскольку в OpenGL основной используемый формат данных - это GLFloat, который определён, как Single, то файл модели должен быть именно file of Single. Поскольку мы собираемся хранить в файле так же данные типа word, то можно заранее ввести ещё один новый тип, который послужит для записи этих данных в файл типа Single. Определить этот тип можно следующим образом: b4=array[1..2] of word;

Далее можно составить процедуры перевода типа b4 в тип Single. Так же, необходимо сохранить в файл строковые данные - это знак модели и её имя. Поэтому так же необходимо ввести возможность сохранения в файл и последующего чтения строковых данных. В файл будут подряд записаны следующие данные: Ключ файла - 4-х байтная строка 'YEM' (в Delphi, как известно, размер строки равен длине строки плюс один байт) Имя модели. Используется следующий принцип. Строка разбивается на отрезки длиной по 3 символа, а затем каждый отрезок записывается в файл как строка. При этом, в начале, сохраняется количество таких отрезков. В следующие 6 байт будут записаны количество вершин модели, количество треугольников и количество вершин на текстурной карте. После этого 2 байта остаются пустыми. Далее записаны координаты вершин сетки по 12 байт на каждую вершину. Треугольники записываются следующим образом. 3 числа по 2 байта означают индексы вершин, ещё один байт оставлен пустым. Вершины на текстурной карте записываются таким же образом, как и вершины сетки (вместо X,Y,Z будут U, V, W). Треугольники на текстурной карте. Их, очевидно, столько же, сколько треугольников у всего тела. В следующие 4 байта записаны 2 2-байтные числа - это количество точек и количество костей. Далее записаны в таком же формате, как и вершины сетки, записаны координаты всех точек скелета. После этого записываются кости. Заголовок каждой кости занимает 4 байта. 2 байта - индекс начальной точки, 2 - индекс конечной точки. После этого ещё 2 байта занимает количество прикреплённых к кости вершин, 2 байта - это индекс части тела, к которой относится данная кость, а после этого перечисляются индексы всех прикреплённых вершин по 2 байта на каждый, а между ними пустые 2-байтные промежутки. Количество частей тела. Названия частей тела, записанные в таком же формате, как имя модели. Данные анимации для каждой части тела. Анимация одной части тела содержит: Количество движений, которые выполняет часть тела (2 байта. +2 байта пустые) Движения данной части тела. Движение включает: Количество ключевых кадров (2 байта. +2 байта пустые) Продолжительность действия - целое число в мс (2 байта. +2 байта пустые) Ключевые кадры каждой части тела. Ключевой кадр включает: Количество передвигаемых костей (2 байта. +2 байта пустые). Индексы этих костей (по 4 байта, из них по 2 байта пустые) Углы поворота этих костей (по 12 байт на каждую кость. По 4 байта на угол в одной плоскости). Момент времени считая от начала движения, к которому относится ключевой кадр (2 байта. +2 байта пустые).

Теперь можно написать процедуру чтения данных из такого файла. Для начала, введём тип b4 и напишем подпрограммы для преобразования его в тип Single для записи в файл, а так же функцию, которая будет читать из файла строку. Procedure TModel.LoadFromFile(filename:string); //4-х байтный тип, преобразуемый в Single type b4=array[1..2] of word; //Получить два целых числа - старшую и младшую пару //из двойного слова Single Function Getb4ofd(n:single):b4; var p1:pointer; p2:^b4; begin p1:=@n; p2:=p1; Getb4ofd:=p2^; end; //Получить строку из 3-х символов из Function GetStrOfD(n:single):string; type s3=string[3]; var p1:pointer; p2:^s3; begin p1:=@n; p2:=p1; GetStrOfD:=p2^; end; var //Переменные для доступа к файлу модели Function ReadString:ansistring; var x:single; l, k:integer; res:string; //Получить целое двойное слово из числа Single Function GetLIOfS(n:Single):LongInt; var p1:pointer; p2:^longint; begin p1:=@n; p2:=p1; GetLIOfs:=p2^; end; begin res:=''; read(t, x); l:=GetLIOfS(x); for k := 1 to l do begin read(t, x); res:=res+GetStrofD(x); end; ReadString:=res; end;

После этого можно прочитать модель из файла, пользуясь описанием её формата. Begin //Открыть файл для чтения assignfile(t, filename); reset(T); //Прочитать заголовок read(t, n); //Если он не такой, какой нужен, следовательно, //файл не формата модели if getStrOfD(n)<>'YEM' then begin closefile(t); exit; end; //Прочитать из файла имя модели ModelName:=readstring; //Прочитать из файла данные о сетке read(t, body.Empty); //Пустое место read(t, n); //Получить количество вершин body.VertexCount:=Getb4ofd(n)[1]; //количество треугольников body.FacesCount:=Getb4ofd(n)[2]; read(t, n); //И количество вершин на карте текстуры body.TexVertexCount:=Getb4ofd(n)[1]; //Выделить подо всё это память GetMem(body.Vertices, body.VertexCount*SizeOf(TVertex)); GetMem(body.Faces, body.FacesCount*SizeOf(TFace)); //Необходимо выделить память и для переменных //временного назначения GetMem(body.DeformatedVertices, SizeOf(TVertex)*body.VertexCount); GetMem(body.TexVertices, body.TexVertexCount*SizeOf(TVertex)); GetMem(body.TexFaces, body.FacesCount*Sizeof(TFace)); for i := 0 to body.VertexCount-1 do begin //Ввести координаты вершин сетки read(t, body.Vertices[i].x); read(t, body.Vertices[i].y); read(t, body.Vertices[i].z); end; for i := 0 to body.FacesCount-1 do begin //Ввести индексы вершин треугольников read(t, n); body.Faces[i][0]:=Getb4ofd(n)[1]; body.Faces[i][1]:=Getb4ofd(n)[2]; read(t, n); body.Faces[i][2]:=Getb4ofd(n)[1]; end; for i := 0 to body.TexVertexCount-1 do begin //Ввести координаты текстуры read(t, body.TexVertices[i].x); read(t, body.TexVertices[i].y); read(t, body.TexVertices[i].z); end; for i := 0 to body.FacesCount-1 do begin //Ввести треугольники на текстурной карте read(t, n); body.TexFaces[i][0]:=Getb4ofd(n)[1]; body.TexFaces[i][1]:=Getb4ofd(n)[2]; read(t, n); body.TexFaces[i][2]:=Getb4ofd(n)[1]; end; read(t, n); //Получить количество точек body.PointCount:=Getb4ofd(n)[1]; //и количество костей body.BoneCount:=Getb4ofd(n)[2]; //Выделить для их хранения память GetMem(body.Points, body.PointCount*sizeof(SkPoint)); GetMem(body.Bones, body.BoneCount*SizeOf(SkBone)); for i := 0 to body.PointCount-1 do begin //Прочитать координаты точек read(t, body.Points[i].x); read(t, body.Points[i].y); read(t, body.Points[i].z); end; for i := 0 to body.BoneCount-1 do begin //Прочитать индексы точек начала и конца кости read(t, n); body.Bones[i].StartPoint:=Getb4ofd(n)[1]; body.Bones[i].EndPoint:=Getb4ofd(n)[2]; read(t, n); //Получить количество привязанных вершин body.Bones[i].numVertices:=Getb4ofd(n)[1]; GetMem(body.Bones[i].VertArray, 2*body.Bones[i].numVertices); for j := 0 to body.Bones[i].numVertices-1 do begin //Получить индексы вершин сетки,привязанных к кости read(t, n); body.Bones[i].VertArray[j]:=Getb4ofd(n)[1]; end; end; read(t, n); //Получить количество частей тела numBodyParts:=Getb4ofd(n)[1]; //Прочитать названия частей тела for i := 0 to numBodyParts-1 do readString; //Выделить память для временного хранения //углов поворота костей в данный момент времени GetMem(body.DeformationBoneState, body.BoneCount*SizeOf(TBoneState)); //Обнулить эти ячейки памяти for i := 0 to body.BoneCount-1 do begin body.DeformationBoneState[i].AngleYOZ :=0; body.DeformationBoneState[i].AngleXOZ :=0; body.DeformationBoneState[i].AngleXOY :=0; end; //Ввести данные анимации для каждой части тела for i := 0 to numBodyParts-1 do begin read(t, n); //Ввести количество движений, которые может совершать //данная часть тела numActions[i]:=Getb4ofd(n)[1]; for j := 0 to numactions[i]-1 do begin //Выделить память для хранения движения GetMem(BodyParts[i][j], SizeOf(TBodyPartAction)); read(t, n); //Ввести из файла продолжительность движения и //количество ключевых кадров BodyParts[i][j].Duration:=Getb4ofd(n)[1]; BodyParts[i][j].KeyFrameCount:=Getb4ofd(n)[2]; //Ввод ключевого кадра for m := 0 to BodyParts[i][j].KeyFrameCount-1 do begin read(t, n); //Ввести количество задействованных в ключевом //кадре костей BodyParts[i][j].KeyFrames[m].KFTimer:= Getb4ofd(n)[1]; //и момент времени, в который выполняется //этот ключевой кадр BodyParts[i][j].KeyFrames[m].BoneCount:= Getb4ofd(n)[2]; //Выделить память для хранения ключевого кадра GetMem(BodyParts[i][j].KeyFrames[m].BoneState, BodyParts[i][j].KeyFrames[m] .BoneCount*SizeOf(TBoneState)); GetMem(BodyParts[i][j].KeyFrames[m].BoneIndexes, BodyParts[i][j].KeyFrames[m].BoneCount*2); for v := 0 to Bodyparts[i][j].KeyFrames[m] .BoneCount-1 do begin //Получить данные о повороте костей //в ключевом кадре read(t, n); BodyParts[i][j].KeyFrames[m].BoneState[v] .AngleYOZ:=n; read(t, n); BodyParts[i][j].KeyFrames[m].BoneState[v] .AngleXOZ:=n; read(t, n); BodyParts[i][j].KeyFrames[m].BoneState[v] .AngleXOY:=n; read(t, n); //и индексы этих костей Bodyparts[i][j].KeyFrames[m].BoneIndexes[v]:= GetB4ofd(n)[1]; end; end; end; end; closefile(t); end;

Теперь можно реализовать подпрограмму TimeLeft, однако, в силу её простоты, я надеюсь, читатель сможет сам в ней разобраться. Я же перейду к подпрограмме PresetRotateAngles, которая должна совершать следующие действия. Как мы знаем, в массиве Timers хранятся значения времени выполнения действия каждой частью тела. Программа должна определить, соответствуют ли эти моменты времени ключевым кадрам. Если да, то она просто переписывает углы поворота в массив DeformatedBonesState подобъекта Body, а если текущему моменту не соответствует ключевой кадр, программа должна определить два ближайших ключевых кадра и вычислить угол поворота кости. Напомню, что в условии нигде не говорилось о том, что ключевые кадры отсортированы в файле по возрастанию KFTimer. Поскольку потребуется создавать каким-то образом модели, то пользователь вряд ли сможет сразу узнать, в какие моменты времени потребуется вставить ключевые кадры. Поэтому при создании ключевые кадры не отсортированы. Итак, приступим. Введём три переменные для цикла и 9 переменных Single, которые будут хранить углы поворота в следующий момент времени, предыдущий и текущий. Кроме того, для поиска соседних ключевых кадров нам потребуются две переменные, которые как-то запомнят эти кадры, а, если ключевой кадр будет найден, его так же потребуется сохранить. Procedure TModel.PresetRotateAngles(DeltaTimer:word); var i, j, k:integer; Anglex1, anglex2, angley1, angley2, anglez1, anglez2, anglex, angley, anglez:single; minupper, maxlower:integer; KeyFrameIndex:integer;

Теперь нужно увеличить значение всех таймеров на DeltaTimer. А если при этом таймер переходит за предел времени, требуется вернуть его назад. for i := 0 to numBodyParts-1 do begin Timers[i]:=Timers[i]+DeltaTimer; if Timers[i]>BodyParts [i, CurrentActions[i]].Duration then Timers[i]:=Timers[i]-BodyParts [i, currentActions[i]].Duration; end;

После этого можно попробовать найти ключевой кадр, соответствующий данному моменту. Если он найден, можно сразу переписать значения. Иначе - их придётся вычислить. ()

Теперь значения углов поворота костей были записаны в массив DeformationBoneState подобъекта Body. Теперь можно составить алгоритм поворота этих костей. Модель должна содержать такую вершину скелета, положение которой не зависит ни от одной кости. Программа находит эту точку и пробует повернуть кости, которые начинаются в этой точке. По пути, она поворачивает и все кости, которые начинаются из конца текущей. Определим рекурсивную процедуру поворота костей. Procedure TModel.Draw(x1, y1, x2, y2, mx1, my1, mx2, my2:single); Procedure RotateBone(BoneIndex:word); var R_i:integer; //Чтобы не перепутать циключескую //переменную из тела основной процедуры, //Добавим к этой префикс x, y, z:single; x0, y0, z0, d, alpha0:single; begin for R_i := 0 to Body.BoneCount-1 do //Если другая кость выходит из конца данной кости, //провести эту процедуру с ней if Body.Bones[R_i].StartPoint=Body.Bones [BoneIndex].EndPoint then RotateBone(R_i); //Получить координаты точки, вокруг //которой совершить поворот x:=Body.Points[Body.Bones[BoneIndex].StartPoint].x; y:=Body.Points[Body.Bones[BoneIndex].StartPoint].y; z:=Body.Points[Body.Bones[BoneIndex].StartPoint].z; for R_i := 0 to Body.Bones[BoneIndex].numVertices-1 do begin //Получить координаты поворачиваемой точки x0:=Body.DeformatedVertices [Body.Bones[BoneIndex].VertArray[R_i]].x; y0:=Body.DeformatedVertices [Body.Bones[BoneIndex].VertArray[R_i]].y; z0:=Body.DeformatedVertices [Body.Bones[BoneIndex].VertArray[R_i]].z; //Совершить поворот отдельно //в трёх различных плоскостях d:=sqrt((y0-y)*(y0-y)+(z0-z)*(z0-z)); alpha0:=arctan((z0-z)/(y0-y)); if y0-y<0 then begin if alpha0<0 then alpha0:=alpha0+pi else alpha0:=alpha0-pi; end; alpha0:=alpha0+Body.DeformationBoneState [BoneIndex].AngleYOZ; y0:=y+d*Cos(alpha0); z0:=z+d*sin(alpha0); d:=sqrt((x0-x)*(x0-x)+(z0-z)*(z0-z)); alpha0:=arctan((z0-z)/(x0-x)); if x0-x<0 then begin if alpha0<0 then alpha0:=alpha0+pi else alpha0:=alpha0-pi; end; alpha0:=alpha0+Body.DeformationBoneState [BoneIndex].AngleXOZ; x0:=x+d*Cos(alpha0); z0:=z+d*sin(alpha0); d:=sqrt((x0-x)*(x0-x)+(y0-y)*(y0-y)); alpha0:=arctan((y0-y)/(x0-x)); if x0-x<0 then begin if alpha0<0 then alpha0:=alpha0+pi else alpha0:=alpha0-pi; end; alpha0:=alpha0+Body.DeformationBoneState [BoneIndex].AngleXOY; x0:=x+d*Cos(alpha0); y0:=y+d*sin(alpha0); //Сохранить координаты вершин после поворота Body.DeformatedVertices[Body.Bones [BoneIndex].VertArray[R_i]].x:=x0; Body.DeformatedVertices[Body.Bones [BoneIndex].VertArray[R_i]].y:=y0; Body.DeformatedVertices[Body.Bones [BoneIndex].VertArray[R_i]].z:=z0; end; end; var PointsDerivation:array[word] of boolean; TmpVertex:TVertex; TmpFace:TFace; TmpTexVertex:TVertex; TmpTexFace:TFace; i, j, k:integer; begin for i := 0 to Body.VertexCount-1 do //Записать значения вершин до деформации Body.DeformatedVertices[i]:=Body.Vertices[i]; //Найти независимую точку for i := 0 to Body.PointCount-1 do PointsDerivation[i]:=false; for i := 0 to Body.PointCount-1 do for j := 0 to Body.BoneCount-1 do if Body.Bones[j].EndPoint = i then PointsDerivation[i]:=true; for i := 0 to Body.BoneCount-1 do //Совершить поворот костей, //начинающихся от независимой точки if not PointsDerivation[Body.Bones[i].StartPoint] then RotateBone(i);

Теперь следует расхождение. Одна из подпрограмм рисует тело с маскировкой, другая - без неё. Честно скажу, не уверен в том, что процедура рисования сетки с маскировкой будет работать - была написана наспех. Однако первая процедура проверена. В силу её простоты я не буду её здесь приводить. В силу того, что рисование объектов с маскировкой не входит в тему настоящей статьи, считаю, что читатель сам сможет в ней разобраться, используя статьи соответствующей тематики. Так же считаю ненужным приводить здесь комментарии к процедуре Destroy. Ломать всегда легче, чем строить. Выгружать почти всегда легче, чем загружать. На этом могу сказать, что модуль для работы с моделью готов.

Однако сам модуль не умеет делать ровным счетом ничего. Сначала нужно составить, во-первых, программу для разработки таких моделей, а, во-вторых, какую-нибудь программу, которая бы использовала этот модуль. Скажу несколько слов только по поводу названной программы. Она составлена. И включена в пакет, включенный в статью. Эта программа загружает из текстового файла сетку и позволяет создать к ней скелет, а к скелету - анимацию. Сразу скажу, работать в ней - не в потолок плевать. Это Вам не MAX. Текстовые файлы должны иметь тип EYE, и следующий формат. В первой строке файла записаны количество вершин сетки, количество полигонов и количество вершин на карте текстуры. После этого в текстовом виде перечислены все необходимые данные. Заранее прошу прощения за то, что программа на английском языке. Дело в том, что, по крайней мере на моей машине, в двух операционных системах - Windows 98 и Windows XP, - разные кодировки русских букв, поэтому программа, составленная под WinXP, под Win98 выводит на экран иероглифы. Для создания файл EYE из 3DStudioMax можно воспользоваться поставляемым макросом. Идея макроса и экспорта сетки с текстурой заимствованы из статьи .

Теперь скажу пару слов о тестовой программе. Она загружает файл модели и файл текстуры. Нельзя, чтобы модель была привязана к текстуре. Если модель не привязана к текстуре, то, пользуясь различными текстурами, можно, надевая их на одну и ту же модель, создавать разные объекты. Например, на сетку, которая могла бы быть лейтенантом, кроме текстуры лейтенанта можно надеть текстуры капитана, майора, и т.д. Загружаемая тестовой программой модель содержит слегка анимированного террориста из Counter Strike. Я говорю слегка потому, что он может выполнять совсем мало действий, однако этого достаточно, чтобы продемонстрировать возможности модуля. Надетая на него текстура является смесью текстуры персонажа и текстуры дробовика, позаимствованной мною из игры Medal of Honour. После этого при нажатии на форме появляется человек, вооружается и начинает бежать, наставив на кого-то прицел. Программа ведёт подсчёт FPS и выводит его в заголовке. На процессоре Pentium 1.7 GHz мне удалось выжать из программы скорость 110 FPS, однако такое случилось только один раз, когда я под WinXP выгрузил из памяти все ненужные службы. В обычном же режиме скорость составляла около 50, если на форму был наведён курсор мыши и 62, если курсор был убран с экрана. Можно сказать, что модель не обладает таким уж высоким быстродействием, как модели Quake MD3, однако это отражается и на объёме файла модели. Кроме того, программа имеет один серьёзный недостаток. При запуске таймера нажатием на форму FPS равна 0, поэтому иногда человек начинает с феноменальной скоростью махать ружьем и ногами, но это продолжается лишь секунду.

Дальше я скажу о том, как создать такого человека с помощью поставляемого комплекта. Во-первых, я буду использовать 3DStudioMax. Если у Вас нет этой программы, то можете использовать заранее экспортированный файл сетки ManArmed.eye. Запустим программу EditMdl.exe. Выберете в открывшемся окне фильтр файлы *.eye и откройте файл ManArmed.eye. В следующем окне введите рост Вашего персонажа. Я выбрал 1.8 (или 2? - забыл). Наконец, в следующем окне выберете файл текстуры g2.bmp. Затем, если в открывшемся окне редактора выбрать вид Перспективы, перед нами предстаёт грозного вида мужчина с опасной игрушкой. ()

Теперь необходимо создать вершины скелета. Для данной модели это удобнее всего делать в проекции Left. Замечу, что заполнение полей x, y, z производится следующим образом. Когда курсор стоит в окне x, то при щелчке мышью на окне проекции заполняются все поля. Если курсор стоит в окне y, то заполнятся только y и z. Если курсор установлен в окно z, то только это окно и заполняется. Однако, не забывайте, что в проекции Left создаваемой точке будет присваиваться координата x=0. Координаты указателя в пространстве можно видеть в верхней полосе дисплея. Далее необходимо создать кости. Для выбора точки скелета в качестве начала или конца создаваемой кости, достаточно выделить точку с помощью мыши. Аналогично выбираются вершины сетки. Помните, что при выделении с вершинами будет проделана та операция, которая выбрана в окне опций.

После того, как скелет создан, в проекции Left вы можете увидеть нечто похожее на это:

Самое время сохранить изменения. Обратите внимание, что программа сохраняет изменения в файл формата sks. Это связано с тем, что этот файл не является текстовым экспортированным файлом, и не является ещё файлом модели. Теперь нажмите кнопку Части Тела и распределите кости между двумя частями тела: UpperBody и LowerBody. Конечно, можно было создать и голову, но зачем? Ведь наш персонаж не имеет рта и не умеет разговаривать. Можно перейти в другой режим, нажав кнопку Animate. При этом необходимо ввести имя создаваемой модели. Это необходимо, если мы захотим, чтобы нашей моделью затем управлял составленный на языке VBScript сценарий. Нажав на ОК, Вы попадёте в режим анимации.

Думаю, этот режим достаточно прост для понимания. Нужно выбрать часть тела и добавить движения. После того, как указана длительность, действия программы похожи на действия 3DStudioMax, поэтому дальнейший процесс не описывается. Полученный мною результат хранится в файле 1.yem, и Вы можете сразу же загрузить его с помощью тестовой программы.

Редактор содержит так же множество недостатков. Один из них - это то, что окна редактора всегда прячутся под другие открытые окна. Если Вы, открыв редактор и выбрав в первом окне файл модели, не обнаружили заметной реакции программы, нужно с помощью Alt+Tab переключиться на значок Кусок Мяса И Кость. Другой недостаток - это то, что иногда окна прячутся под вид, отображаемый OpenGL.

Текст программы EditMdl я не привожу по нескольким причинам. Эта диалоговая программа не имеет никаких сложных алгоритмов, кроме алгоритма костных деформаций, зато содержит массу однообразного текста программы, который совершенно не интересен. Кроме того, хочу оставить при себе алгоритмы и методы работы этой программы.

Использованная литература. 3d Studio Max 3 для профессионалов Самоучитель Программирование в Turbo Pascal 7.0 и Delphi Статья

Использованные материалы. Модель позаимствована из уже названной статьи. Текстура ружья Winchester позаимствована у ID Software (Medal of Honor Allied Assault) Скачать (433К).
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
Внимание! Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.
Элитные от интернет-магазина, проверенным временем.



Эксперимент


Мой эксперимент заключается в следующем. Создать 3d-движок с нуля. Это значит, что нужно, ни на что не опираясь, ввести формат тел, формат анимации, комплекс процедур для реализации в движке методов аналитической геометрии и физики. И, двигаясь по порядку, я начинаю с создания формата моделей



Принцип костной деформации


Тот, кто работает в программе 3dStudioMax, отлично знает, что это такое. Для начала создаётся сетка - множество точек в пространстве и треугольники, вершинами которых являются эти точки. Затем создаются кости. Каждая кость имеет начальную точку и конечную точку. Затем, выражаясь языком MAX, применив модификатор Skin, мы привязываем к каждой кости определённое множество вершин сетки, и, после этого, при движении скелетных костей вместе с ними двигаются и привязанные к ним точки, поворачиваясь вокруг точек начал костей.

Теперь скажу о том, как принцип костной деформации будет реализован в моём алгоритме. В пространстве будут определены точки. Затем эти точки будут соединены костями. Для каждой кости будут определены начало, конец и привязанные вершины. Итак, следуют следующие определения: //Определение костей SkPoint=record //Точка имеет три координаты в пространстве x, y, z:double; end; PSkPointArray=^TSkPointArray; TSkPointArray=array[word] of SkPoint; PVertArray=^TVertArray; //Список привязанных вершин определим как множество //индексов этих точек TVertArray=array[word] of word; SkBone=record //Начало и конец кости - индексы точек скелета StartPoint:word; EndPoint:word; //Количество вершин сетки, привязанных к кости numVertices:word; //Массив индексов этих вершин VertArray:PVertArray; end; PSkBoneArray=^TSkBoneArray; TSkBoneArray=array[word] of SkBone; //Определение сетки TVertex=record x, y, z:single; end; PVertexArray=^TVertexArray; TVertexArray=array[word] of TVertex; TFace=array[0..2] of word; PFaceArray=^TFaceArray; TFaceArray=array[word] of TFace; //Эта запись определяет угол поворота кости и //привязанных точек в соответствующей плоскости TBoneState=record AngleYOZ, AngleXOZ, AngleXOY:single; end; PBoneStateArray=^TBoneStateArray; TBoneStateArray=array[word] of TBoneState;

Теперь определим основную структуру для сетки со скелетом. Она должна содержать массив вершин, массив полигонов, массив вершин текстуры, массив полигонов текстуры, массив точек скелета, массив костей, а так же временный массив для хранения координат вершин после деформации. SkinnedMesh=record //Количество вершин сетки VertexCount:word; //Количество полигонов FacesCount:word; //Количество вершин на текстурной карте TexVertexCount:word; //Массив вершин сетки Vertices:PVertexArray; //Массив треугольников сетки Faces:PFaceArray; //Массив вершин на текстурной карте TexVertices:PVertexArray; //Массив треугольников на текстурной карте TexFaces:PFaceArray; //Количество точек скелета PointCount:word; //Массив точек скелета Points:PSkPointArray; BoneCount:word; //Количество костей Bones:PSkBoneArray; //Массив костей Empty:single; //Не используется //Массив для хранения DeformationBoneState:PBoneStateArray; //углов поворота костей DeformatedVertices:PVertexArray; //Массив для хранения //координат вершин деформированной сетки end;

После того, как мы получим сетку и скелет, можно приступить к описанию самих структур движений. Весь скелет мы поделим на части тела. Так можно будет экономичнее записать модель. Тогда можно будет, например, заставить торс и ноги персонажа выполнять разные действия. К примеру, человеческую модель можно разделить на то, что ниже пояса, выше пояса и голову. Каждая часть дела может выполнять различное количество действий. Каждое действие задаётся изменением положения костей.

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

Допустим, что имеются кости 0, 1, 2, 3, 4. Из них кости 0, 1, 3 принадлежит части тела Upper_body, оставшиеся - Lower_body. Тогда массив индексов костей в ключевом движении Upper_body будет содержать (0, 1, 3), а Lower_body - (2,4). Итак, далее следует новые определения. PWordArray=^TWordArray; //Ключевое положение части тела BodyPartKeyFrame=record BoneCount:word; //Массив положений костей в данный ключевой кадр BoneState:PBoneStateArray; //Массив индексов костей BoneIndexes:PWordArray; //Момент времени, которому соответствует //данный ключевой кадр KFTimer:integer; end; PBodyPartAction=^TBodyPartAction; //Действие, выполняемое одной из частей тела TBodyPartAction=record //Продолжительность этого действия Duration:integer; //Количество ключевых кадров в действии KeyFrameCount:word; //Множество ключевых кадров KeyFrames:array[byte] of BodyPartKeyFrame; end; //Основной класс модели TModel=class Body:SkinnedMesh; //Сетка и скелет модели //Данные для анимации модели BodyParts:array[byte] of array[byte] of PBodypartAction; //Количество частей тела numBodyParts:byte; //Массив, содержащий количество движений //каждой части тела numActions:array[byte] of byte; //Имя модели ModelName:ShortString; //Текущие действия, выполняемые разными частями тела CurrentActions:array[byte] of byte; //Момент действия каждой части тела Timers:array[byte] of word; //Процедура осуществляет загрузку модели из файла Procedure LoadFromFile(filename:string); //Функция возвращает время, оставшееся до того, //как одна из частей тела //закончит выполнять текущее действие Function TimeLeft:word; //Записать углы поворота каждой кости в массив //DeformationBoneState из Body Procedure PresetRotateAngles(DeltaTimer:word); //Нарисовать модель Procedure DrawModel(x1, y1, x2, y2:single); overload; //Нарисовать модель, используя маску текстуры Procedure DrawModel(x1, y1, x2, y2, mx1, my1, mx2, my2:single); overload; //Процедура осуществляет стирание из памяти //всех данных и удаление класса Destructor Destroy; override; end;

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



Улучшение вспомогательных окон среды Delphi


Владимир Коднянко,

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

для ускорения программирования или отладки программы обычно возникает потребность в том, чтобы с наименьшими затратами времени программировать вывод констант и значений переменных наиболее часто используемых типов (обычно строковых и числовых) с помощью одной или нескольких "подручных" подпрограмм, не тратя время на конвертацию из одного типа в другой (чаще строковый); для большинства случаев это можно сделать воспользовавшись, например, типом Variant; использование стандартных подпрограмм, например ShowMessage, иногда не удовлетворяет программиста по той причине, что это окно всегда выводится в центре экрана, и если окно приложения находится в этот момент не в центре, а в каком-нибудь углу экрана, то такое расположение окон нежелательно; можно, конечно, воспользоваться другой подпрограммой, позволяющей позиционировать окно где угодно, но "угадать", где в данный момент находится активное окно, обычными средствами непросто; наиболее приемлемой можно считать ситуацию, когда окно вопроса или сообщения имеет общий центр с активной формой, однако "не теряется" за пределами экрана если в большом окне активной формы ее центр находится вне экрана; площадь стандартных окон достаточно велика из-за неоправданно низкого расположения рисунка и кнопок в окне, а также довольно большого расстояния от кнопок до нижнего края окна; можно также улучшить вывод надписи на метке, позиционируя ее по отношению к рисунку в зависимости от числа строк на метке; такие изменения позволят, во-первых, уменьшить высоту окна и, во-вторых, улучшить расположение надписи на нем; если на компьютер устанавливается Delphi (англоязычная), то чтобы надписи в окнах сообщений и вопросов (в заголовках, на кнопках) были русскоязычными, надо затратить дополнительные усилия по русификации надписей, что требует отдельной работы: здесь желательно иметь подпрограммы, которые способны сразу "выдавать" надписи в окнах на русском языке вне зависимости от того, русифицирована Delphi или нет.

Разрешение этих "но" является целью настоящего сообщения.

Прежде нужно создать новый unit или добавить низлежащий код в уже имеющийся подходящий unit и объявить несколько переменных, которые потребуются для автоматической русификации надписей. Их лучше разместить в секции implementation выше текстов приведенных ниже подпрограмм.

var // кнопки ButtonEngCaptions: array[1..11] of string = ('Yes', 'No', 'OK', 'Cancel', 'Abort', 'Retry', 'Ignore', 'All', 'NoToAll','YesToAll', 'Help'); ButtonRusCaptions: array[1..11] of string = ('Да', 'Нет', 'OK', 'Отмена', 'Прервать','Повтор', 'Пропуск', 'Все', 'Нет Всем','Да Всем', 'Помощь'); // заголовки окон MsgEngCaptions: array[1..4] of string = ('Confirm', 'Information', 'Warning', 'Error'); MsgRusCaptions: array[1..4] of string = ('Подтвердите', 'Сообщение','Предупреждение','Ошибка');

Далее возьмем стандартную функцию MessageDlgPosHelp модуля Dialogs.pas и коррекцией ее кода создадим новую функцию KdnMessageDlg (текст функции снабжен необходимыми комментариями):

function KdnMessageDlg(MsgVariant: string; DlgType: TMsgDlgType; Buttons: TMsgDlgButtons): Integer; var w1,w2,h1,h2,t2,L2,cx,cy: Integer; ScreenActFormVisBoo: boolean; i,j: Integer; F: TForm; Msg,s: ^String; begin New(Msg); New(s); Msg^:= MsgVariant; // конвертируем Variant в строку F:= CreateMessageDialog(Msg^,DlgType,Buttons); with F do try w1:=0; w2:=0; h1:= 0; // рабочие переменные // русифицируем надпись на шапке F-формы for i:= 1 to 4 do if Caption = MsgEngCaptions[i] then Caption:= MsgRusCaptions[i]; // изменяем положение элементов формы и русифицируем кнопки for i:= 0 to F.ComponentCount-1 do begin // приподнимаем рисунок if F.Components[i] is TImage then With F.Components[i] as TImage do Top:= Top-4; // позиционируем метку относительно рисунка // в зависимости от числа строк if F.Components[i] is TLabel then With F.Components[i] as TLabel do begin w1:=1; // вычислим число строк в метке if Length(Caption)>2 then for j:= 1 to Length(Caption)-2 do if Copy(Caption,j,2) = #13#10 then Inc(w1); if w1=1 then Top:= Top+2 else if w1=2 then Top:= Top-2 else Top:= Top-4; w2:= Top+height; // положение нижней части метки end; // русифицируем надписи на кнопках и позиционирум кнопки // в зависимости от числа строк метки if F.Components[i] is TButton then With F.Components[i] as TButton do begin s^:= Caption; // приведем надпись к виду ButtonEngCaptions Delete(s^,Pos('&',s^),1); s^:= AnsiUpperCase(DelSymbAll(s^,' ')); for j:=1 to 11 do // поиск надписи if s^ = AnsiUpperCase(ButtonEngCaptions[j]) then Caption:= ButtonRusCaptions[j]; // русификация if w1=1 then Top:= w2+20 else // позиционирование if w1=2 then Top:= w2+12 else Top:= w2+10; h1:= Top+height; // положение нижней части кнопок end; end; // for i height:= h1+42; // подбираем подходящую высоту формы // вычисляем положение F-формы // 1. определяем центр активной формы cx:= -1; cy:= -1; // координаты центра активной формы ScreenActFormVisBoo:= false; // наличие и видимость активной формы if Screen.ActiveForm <> Nil then if Screen.ActiveForm.Visible then begin w2:= Screen.ActiveForm.width; h2:= Screen.ActiveForm.height; t2:= Screen.ActiveForm.Top; L2:= Screen.ActiveForm.Left; cx:= L2 + w2 div 2; // координаты центра активной формы cy:= t2 + h2 div 2; ScreenActFormVisBoo:= true; end; // 2. определяем координаты левого верхнего угла F-формы w1:= width; h1:= height; // параметры F-окна if ScreenActFormVisBoo then // активная форма видима begin w2:= Screen.width; // размеры экрана h2:= Screen.height; Top:= cy - h1 div 2; // F.Top Left:= cx - w1 div 2; // F.Left // F-окно должо быть полностью в экране if Top<0 then Top:=0 else if Top>h2-h1 then Top:= h2-h1; Left:= cx - w1 div 2; if Left<0 then Left:=0 else if Left>w2-w1 then Left:= w2-w1; end else Position:= poScreenCenter; // активной формы нет или невидима Result:= ShowModal; finally // освобождаем память Dispose(Msg); Dispose(s); F.Free; Application.ProcessMessages; // убираем следы F-окна end; end;

где функция DelSymbAll имеет код

function DelSymbAll(s: String; Ch: Char): String; // удаляет символ везде var i: Integer; begin i:= pos(Ch,s); while i>0 do begin Delete(s,i,1); i:= pos(Ch,s); end; Result:= s; end;

Теперь всякое окно, построенное на основе функции KdnMessageDlg, будет иметь с активной формой общий центр, за исключением тех случаев, когда центрирование увело бы любую часть F-окна за пределы экрана (F-окно будет всегда находиться полностью в экране), все надписи русифицированы, метка "правильно" позиционирована относительно рисунка.

Используя KdnMessageDlg построим процедуру - усовершенствованый аналог стандартной процедуры ShowMessage:

procedure KdnMessage(Msg: Variant); //однострочное сообщение begin KdnMessageDlg(Msg, mtInformation,[mbOK]); end;

Несколько примеров обращения к процедуре:

KdnMessage(24); // числовой целочисленный тип аргумента KdnMessage(-224.89); // числовой вещественный тип аргумента KdnMessage('Это строка'); // строковый тип KdnMessage(Now); // тип TDateTime KdnMessage(Tim); // тип TTime KdnMessage(Dat); // тип TDate

В последнем случае активное окно и нависающее над ним окно сообщения будут выглядеть так (центры активной формы и окна сообщения совпадают):

На основе предыдущей процедуры построим многострочное сообщение:

procedure KdnMessageV(Msg: array of Variant); //многострочное сообщение begin KdnMessage(DinVarArrToStrs(Msg); end;

где функция DinVarArrToStrs имеет код:

function DinVarArrToStrs(a: array of Variant): Variant; // конвертация Variant-массива в многострочный Variant var s: array of String; i: byte; begin SetLength(s,2); s[0]:=''; if Length(a)>0 then begin s[0]:= a[0]; if Length(a)>1 then for i:= 1 to Length(a)-1 do begin s[1]:= a[i]; s[0]:= s[0]+''#13#10''+s[1]; end; end; Result:= s[0]; s:= Nil; end;

Пример обращения к процедуре:

KdnMessageV([1355,-15.87,Now,DateOf(Now),TimeOf(Now)]);

и окно, отображающее результат обращения:

Аналогичным образом создадим однострочное окно для вывода вопроса с целью получения ответа от пользователя программы

function KdnYesNo(Question: Variant): boolean; // однострочный вопрос begin Result:= KdnMessageDlg(Question,mtConfirmation, [mbYes,mbNo]) = mrYes; end;

и соответствующее многострочное окно

function KdnYesNoV(Question: array of Variant): boolean; // многострочный вопрос begin Result:= KdnYesNo(DinVarArrToStrs(Question)); end;

Примеры обращения к функциям:

if KdnYesNo('Удалить рисунок ?') then DeleteFile(ImFile); if not KdnYesNoV(['Вы действительно желаете', 'удалить непустую папку', ExeDir,'?']) then exit;

Соответствующие окна показаны ниже.

Точно также можно создать окна с тремя кнопками:

function KdnYesNoCancel(Question: Variant): byte; // однострочное окно с тремя кнопками var r: Integer; begin r:= KdnMessageDlg(Question,mtConfirmation, [mbYes,mbNo,mbCancel]); Result:= 3; // на случай выхода вне кнопок if r = mrYes then Result:= 1 else if r = mrNo then Result:= 2; end; function KdnYesNoCancelV(Question: array of Variant): byte; // многострочное окно с тремя кнопками begin Result:= KdnYesNoCancel(DinVarArrToStrs(Question)); end;

Ограничимся примером обращения к последней функции

if KdnYesNoCancelV(['Вы действительно желаете', 'удалить непустую папку', ExeDir,'?']) = 1 then if KdnYesNo('Подтвердите') then DeleteFolder(ExeDir);

Первое окно, которое появится в результате исполнения этого кода, имеет вид:

Аналогично на основе функции KdnMessageDlg могут быть без труда созданы другие подобные процедуры и функции.



В статье предложен код, который


Владимир Коднянко,

В статье предложен код, который позволяет решить несколько вопросов, связанных с улучшением стандартных окон Delphi и надписей на них, русификацией надписей. Одним из важных свойств окон является позиционирование их над центром активной формы приложения.

Несколько слов об обсуждении названной статьи. Нельзя сказать, что оно было продуктивным. Главным образом, обсуждающие демонстрировали свою просвещенность, не вдаваясь особенно в суть предлагаемых решений. И все же один из принявших участие в обсуждении под псевдонимом Gemini высказал интересную мысль о том, что код, изложенный в статье, может быть с успехом использован не только для русификации, но и, вообще, для быстрой иной "национализации" надписей окна окна (такой мысли у меня даже не было).

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

Возможным средством устранения этого недостатка можно считать способ, когда смещенное от центра активной формы вспомогательное окно при следующем вызове появится в том месте, куда его сместили при предыдущем вызове.

Эта задача решается посредством незначительной модернизации описанного в упомянутой статье кода. Возможный вариант такой модернизации приведен ниже.

В секции interface опишем глобальную переменную

// по умолчанию - старый способ var KdnMessageDlgByLastPosition: boolean = false;

Для фиксации местоположения окна в момент его закрытия введем две переменные KdnMessageDlgLeft, KdnMessageDlgTop, определяющие координаты верхнего левого угла окна. Их описание лучше разместить в секции implementation там, где описаны массивы кнопок:

var // координаты верхнего левого угла окна на случай // вывода окна в том месте, // где оно было закрыто в предыдущее появление KdnMessageDlgLeft: Integer = -1; KdnMessageDlgTop : Integer = -1; // кнопки ButtonEngCaptions: array[1..11] of string = ('Yes', 'No', 'OK', 'Cancel', 'Abort', ...

В тело функции KdnMessageDlg добавим необходимый код. Текст функции в сокращенном виде приведен ниже.

function KdnMessageDlg(MsgVariant: string; DlgType: TMsgDlgType; Buttons: TMsgDlgButtons): Integer; ... cx:= L2+w2 div 2; // координаты центра активной формы cy:= t2+h2 div 2; ScreenActFormVisBoo:= true; end; if KdnMessageDlgByLastPosition and not ((KdnMessageDlgLeft = -1) and (KdnMessageDlgTop = -1)) then begin F.Left:= KdnMessageDlgLeft; F.Top:= KdnMessageDlgTop; end else begin w1:= Width; h1:= Height; // параметры F-окна ... end else Position:= poScreenCenter; end; Result:= ShowModal; finally Dispose(Msg); Dispose(s); KdnMessageDlgLeft:= F.Left; // запоминаем координаты угла KdnMessageDlgTop:= F.Top; F.Free; Application.ProcessMessages; end; end;

Больше ничего менять не нужно.

Чтобы окна появлялось там, где их закрыли при предыдущем вызове, нужно прежде один раз выполнить оператор

KdnMessageDlgByLastPosition:= true; Теперь функции KdnMessage, KdnMessageV и прочие потомки KdnMessageDlg будут "работать" по-новому. Если нужно, чтобы после этого все окна стали "работать" по-старому (позиционирование над центром активной формы), следует один раз выполнить оператор

KdnMessageDlgByLastPosition:= false;

Можно вообще ни разу не выполнять ни один из этих операторов. Тогда окна будут "работать" по-старому. Замечу, что модернизированный код предоставляет пользователю приложений, использующих этот код, более широкие возможности, отличается очевидной гибкостью. Если кто-либо из программистов посчитал целесообразным использовать описанный в предыдущей статье код, то можно порекомендовать модернизировать его вышеописанным либо аналогичным способом. Быть может, модернизированный код следует считать основным (по умолчанию). Не исключено также, что этот код может быть признан кем-либо безальтернативным, что позволит убрать переменную KdnMessageDlgByLastPosition и не заботиться о ее значении при разработке приложения.

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


Обход дерева каталогов с прерыванием и возобновлением или "Куда мы идем завтра?"


Паша Звягинцев,

Программист,
просыпаясь утром с сильнейшего похмелья,
начинает с тестирования памяти...

Недавно занимаясь интересной задачкой по написанию службы индексации, столкнулся с интересным вопросом: " А как бы нам поиск заморозить и продолжить после (через минуту, завтра, через месяц)?". Да конечно можно сказать - что у тебя за машина такая, вот у меня дерево каталогов обходит за 3 минуты... Согласен, это не вопрос. Но когда нужно не просто обходить, а еще и выполнять некоторые действия с файлами, да если их на диске 150 тыс. и больше, да еще не загружая процессор на 100%, то время может затянуться до нескольких суток, вот тогда - как быть?

Вот этой теме я и решил посвятить статью. Как оказалось, в Интернете информации по этой теме нет. Либо это слишком просто, либо никому не нужно. Как выяснилось - ни то ни другое.

Со стандартной процедурой обхода дерева сталкивались очень многие

procedure FileFind(path:string); var sr:Tsearchrec;// Описываем структуру, которую // использует для поиска система found:integer; // найдено или нет begin found:=FindFirst(path + '\*.*', FaAnyfile, sr); {по команде FindFirst программа создает структуру следующего типа TsearchRec = record Time: Integer; // время создания Size: Integer; // его размер Attr: Integer;// атрибуты Name:TFileName // = TString; собственно имя файла ExcludeAttr: Integer; найденные атрибуты FindHandle: THandle; // !!! указатель на структуру //поиска, которую создает система, а не наша программа. //Вот для чего обязательно в конце поиска //указывать FindClose - это высвобождает память FindData: TWin32FindData; // собственно эта структура end;} while (found = 0) do // если хоть что-то найдено begin if (sr.name <> '.') and (sr.name <> '..') then begin // если это не указатели на корневые каталоги, // то что-то нашли if (sr.attr and FaDirectory) = FaDirectory then // ага вот поддиректория - вызываем себя рекурсивно, // но с поиском уже // в этой директории FileFind(path+'\'+sr.name) else begin // вот тут выполняем чтото с найденным файлом //...... mainform.memo1.lines.append(path+'\'+sr.name); end end; found:=findnext(sr); // есть ли еще файлы или каталоги end; FindClose(sr); // поиск закончен - нужно освободить память end;

Казалось бы сохранить состояние процедуры поиска просто - достаточно сохранить структуру - sr:TsearchRec, а потом ее восстановить и поиск продолжится.

Первое Однако при даже невнимательном рассмотрении процедуры видно, что она вызывает сама себя - налицо обычная рекурсия. Получается что надо сохранять не одну SearchRec, а несколько. Полдела - сохранить, но ведь нужно и восстановить эти рекурсивные вызовы. Т.е при продолжении поиска построить этакую матрешку из процедур поиска, а потом уже его продолжать. Второе — сама SearchRec. Казалось бы она находится в области данных нашей программы. Да это наполовину верно. Верхняя половина SearchRec действительно лежит в области данных нашей программы и делать мы с ней можем что душе угодно. Это переменные Time: Integer; Size: Integer; Attr: Integer; Name:TFileName; ExcludeAttr: Integer;. А вот вторая ее половина (FindHandle: THandle; FindData: TWin32FindData;) нам не принадлежит -ее генерирует система по нашему запросу FindFirst(.....) и уничтожает по команде FindClose(....). Третий, казалось бы, простой вопрос — SearchRec.Name имеет тип TFileName=TString. Какую длину он имеет? Одни скажут 255, другие 65535. Согласен, и то и другое верно, но не тут. Длина действительно 255. А вот с типом нас нагло обманули. Реально в памяти хранится не TString [255], а PChar {Имя файла}+PChar{его расширение}. Для нас с вами это преобразуется в обычную строку при обращении, и до столкновения с данной ситуацией я свято верил что там TString[255].Кстати в чем разница между Богом и билом гейтсом? Бог не считает себя билом гейтсом ...

И так попробуем решить эти проблемы. Проше всего разбор начать в обратном порядке... (не подумайте превратно, я знаю через что рвут гланды в России...)

Третий вопрос - как сохранить , а потом восстановить SearchRec, если он состоит непонятно из чего. А давайте сделаем свой SearchRec, как нам нужно. А именно так

type // этот тип почти полностью переписывается // со стандартного TSearchRec TMysearchRec = record Time: Integer; Size: Integer; Attr: Integer; Name: string[250];//вот тут обрабатывалось неверно при типе TString, как длина ? ExcludeAttr: Integer; FindHandle: THandle; // в принципе не нужен, но // не будем сильно пугать читателей // сильными отличиями, да и бог // с ними - с восемью байтами FindData: TWin32FindData; end;

но нам еще требуется сохранять несколько переменных самой программы, а именно Found - найдено чтото или нет и Path - с каким параметром нас вызывали, поэтому на основе этого типа делаем еще один

TMyRec_Sea = record Rec_Sea:TMySearchRec; // наша структура поиска path:String[250]; // откуда начинали found:integer; // при остановке нашли чтото или нет end;

Второй вопрос после первого решается не очень красиво, но довольно легко. Да система генерит структуру: FindHandle: THandle; FindData: TWin32FindData. FindData - собственно сама структура и FindHandle - указатель на нее. Пусть система генерит что угодно, если с умом, то можно обойти и это. Многие ли помнят такое INT21h->INT 13H. Думаю вспомнили. При восстановлении поиска дадим команду FindFirst, а потом подменим FindData и остальные поля, не трогая FindHandle, иначе сразу после окончания поиска (!!! ???) получим обращение к недопустимому адресу и вылет программы.

...... // создаем запись для поиска FindFirst(path+'\'+mask, FaAnyfile, sr); delfile:=false; found:=buffer.found; // загоняем в SEARCHREC все кроме FINDHANDLE // (он создается системой) sr.Time:=buffer.rec_sea.Time; sr.Size:=buffer.rec_sea.Size; sr.Attr:=buffer.rec_sea.Attr; sr.Name:=buffer.rec_sea.Name; sr.ExcludeAttr:=buffer.rec_sea.ExcludeAttr; sr.FindData:=buffer.rec_sea.FindData;

Первый вопрос - как же сохранять состояние процедуры при рекурсии?. Давайте сохранять SearchRec в файл и используем принцип магазина (не продуктового, а от автомата калашникова) - последний вошел - первый вышел. Вот примерная структура процедуры при выполняющемся поиске ( при нескольких рекурсивных вызовах)

Findfile('c:\') Findfile('c:\Docs') FindFile(c:\Docs\Delphi') ......

При получении сигнала на остановку процедуры начинают писать в файл в обратном порядке, а именно - FindFile(c:\Docs\Delphi'),Findfile('c:\Docs'),Findfile('c:\'). Примерно так

Findfile('c:\')------------------------------------+ Findfile('c:\Docs')---------------------+ ! FindFile(c:\Docs\Delphi') ---+ ! ! v v v [файл сохранений состояния] [rec1] [rec2] [rec3]

Ну а когда нужно восстановить состояние поиска смотрим не пустой ли файл сохранений, и читаем записи начиная с конца, после прочтения их удаляем. Таким образом поиск по дереву автоматом развернется на столько рекурсивных вызовов, сколько надо, и продолжит поиск.

Да, едва не забыл, как мы узнаем что надо приостановить поиск ? Давайте заведем глобальную переменную Process. Как она станет False - пора останавливаться

Ниже приведена часть модуля с использованием описанных алгоритмов

Unit unit1; ...... var .... process:boolean; // вот глобальная переменная // она и управляет поиском // true - можно // false - стоп с запоминанием состояния ..... procedure FileFind(path:string;resume:boolean); { сканирует диск (вернее дерево каталогов) при вызове PATH - начальный каталог для обхода RESUME - если TRUE - то продолжать сохраненный поиск (тогда значение PATH игнорируется, кроме случая, когда не обнаружен файл сохранения поиска) при установке глобальной переменной PROCESS в false останавливается с запоминанием предыдущего состояния, внимание - РЕКУРСИЯ !!! } const save_ext='.rec'; // в каталоге приложения //создает SAVE файл с именем //приложения и указанным расширением mask='*.*'; type TMysearchRec = record //пришлось написать свой тип SEARCHREC //с NAME фиксированной длины Time: Integer; Size: Integer; Attr: Integer; Name: string[250]; //вот тут обрабатывалось // неверно при типе TString, // как длина ? ExcludeAttr: Integer; FindHandle: THandle; FindData: TWin32FindData; end; TMyRec_Sea = record Rec_Sea:TMySearchRec; path:String[250]; found:integer; delfile:boolean; end; var sr:TSearchRec; RecFile:TFileStream; buffer:tMyRec_Sea; sp,save_file_name:string; found:integer; delfile:Boolean; delfile:Boolean; begin if resume then // возобновить поиск или начать новый begin save_file_name:=ChangeFileExt(ParamStr(0),save_ext); if FileExists(save_file_name) then begin RecFile:=TFileStream.Create(save_file_name, fmOpenReadWrite); // чистим буфер, не важно, необходимо для отладки fillchar(buffer,sizeof(buffer),#0); // читаем сохранение начиная с конца файла RecFile.Seek(-1*sizeof(buffer),soFromEnd); RecFile.Readbuffer(buffer,sizeof(buffer)); path:=buffer.path; sp:=path; // создаем запись для поиска FindFirst(path+'\'+mask, FaAnyfile, sr); delfile:=false; found:=buffer.found; // загоняем в SEARCHREC все кроме FINDHANDLE (он создается системой) sr.Time:=buffer.rec_sea.Time; sr.Size:=buffer.rec_sea.Size; sr.Attr:=buffer.rec_sea.Attr; sr.Name:=buffer.rec_sea.Name; sr.ExcludeAttr:=buffer.rec_sea.ExcludeAttr; sr.FindData:=buffer.rec_sea.FindData; // режем кусок уже прочитали свои данные - другим // они не понадобятся RecFile.Seek(-1*sizeof(buffer),soFromEnd); recfile.Size:=RecFile.Position; // дорезались - дозагружаться неоткуда if RecFile.Size=0 then delfile:=true; RecFile.Free; if delfile then sysutils.DeleteFile(save_file_name); end else // нет сохраненных поисков begin // начинаем новый sp:=path; resume:=false; // тут исправляется разница между C:\ и // C:\DOCS - убираем // последний слэш if sp[length(sp)]='\' then sp:=copy(sp,1,length(sp)-1); found:=FindFirst(sp + '\'+mask, FaAnyfile, sr); end end else begin // новый поиск - пристрелить старые записи save_file_name:=ChangeFileExt(ParamStr(0),save_ext); if fileExists(save_file_name) then sysutils.DeleteFile(save_file_name) ; sp:=path; if sp[length(sp)]='\' then sp:=copy(sp,1,length(sp)-1); found:=FindFirst(sp + '\'+mask, FaAnyfile, sr); end; // закончена подготовка - вперед поиск while (found = 0) and process do begin application.ProcessMessages; if (sr.name <> '.') and (sr.name <> '..') then begin if (sr.attr and FaDirectory) = FaDirectory then begin FileFind(sp+'\'+sr.name,resume); end else begin // ну тут разные действия с найденым файлом mainform.label1.caption:= ('начат разбор '+sp+'\'+sr.name) ; // ................ // закончили действия Application.ProcessMessages; // а вот без этого // мы никогда не узнаем что пора поиск закончить end; end; if process then found:=findnext(sr); end; if not process then // получили сигнал на остановку сканирования нужно запомнить состояние begin save_file_name:=ChangeFileExt(ParamStr(0),save_ext); if not FileExists(save_file_name) then RecFile:=TFileStream.Create(save_file_name,fmCreate) else RecFile:=TFileStream.Create(save_file_name, fmOpenReadWrite); RecFile.Seek(0,soFromEnd); // заполняем буфер текущим состоянием buffer.rec_sea.Time :=sr.Time; buffer.rec_sea.Size :=sr.Size ; buffer.rec_sea.Attr :=sr.Attr ; buffer.rec_sea.Name :=sr.Name ; buffer.rec_sea.ExcludeAttr :=sr.ExcludeAttr ; buffer.rec_sea.FindHandle :=sr.FindHandle ; buffer.rec_sea.FindData :=sr.FindData ; buffer.path:=sp; buffer.found:=found; RecFile.Writebuffer(buffer,sizeof(buffer)); RecFile.Free; end; Application.ProcessMessages; sysutils.FindClose(sr); end;



ADSI


После того как определились, что надо сделать (в данном случае это не составляет труда), встает вопрос о реализации. Первой мыслью было использовать технологию WMI, но после краткого исследования проблемы решено было остановиться на ADSI. Далее вольный перевод нескольких предложений из MSDN:

ADSI - Active Directory Service Interfaces. Микрософт создала набор COM-интерфейсов, предназначенных для доступа к различным службам каталогов.

Служба каталогов - это распределенная система, которая предоставляет средства для поиска и использования сетевых ресурсов различных типов.

Объектная модель ADSI базируется на COM - объектах. Программа клиент управляет объектами через интерфейсы. Следующая таблица перечисляет фундаментальные элементы ADSI.

ИнтерфейсыОписание
IADsИспользуется для идентификации объекта. Как фундаментальный интерфейс, поддерживаемый всеми ADSI объектами, позволяет получить доступ к метаданным объекта, включая описание объекта в схеме Active Directory .
IADsContainerИспользуется для извлечения и управления объектом. Все ADSI объекта - контейнеры требуют использование этого интерфейса для доступа к объектам в контейнере и манипулирования ими.
IADsPropertyListИспользуется для работы со свойствами объекта.

Сложные ADSI объекты могут поддерживать дополнительные интерфейсы.



Постановка задачи


На компьютерах с операционными системами Windows NT x.x при установке создается учетная запись локального администратора, которая имеет неограниченные права на данном компьютере. Если компьютер предполагается использовать в домене, то, как правило, технический персонал устанавливает один и тот же пароль для данной учетной записи. И как правило он не очень сложный. При наличии физического доступа к рабочей станции пароль администратора может быть легко подобран со всеми вытекающими отсюда последствиями. Задача администратора сети - установить достаточно сложный пароль для данной учетной записи и периодически его менять. Если в домене несколько десятков компьютеров, это может занять много времени. Если же в домене несколько сот компьютеров, а часто они еще и географически разнесены, то без автоматизации данного процесса не обойтись.

Определимся, что должна делать программа - утилита. Т.е. составим простой алгоритм работы:

получить список имен компьютеров в домене (возможно отфильтрованный по заданному критерию); подключиться к каждому компьютеру из списка и сменить пароль.

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



Реализация на Delphi.


Задача была реализована на Delphi6 sp2. В процессе работы оказалось, что необходимые функции не описаны в библиотеке. Далее в статье будут приведены описания всех необходимых функций.

4.1 Извлечение имен компьютеров домена из AD.

Первым этапом попытаемся установить связь AD. Для этого воспользуемся функцией ADsGetObject. Описание из MSDN:

HRESULT ADsGetObject( LPWSTR lpszPathName, REFIID riid, VOID** ppObject);

lpszPathName - строка связывания;

riid - идентификатор интерфейса;

ppObject - указатель на указатель интерфейса, возвращаемый функцией.

Эта функция эквивалентна функции GetObject из VB (в данном контексте).Она берет строку связывания и возвращает указатель на запрашиваемый интерфейс. Связывание производится в контексте защиты вызывающего потока, используя опции ADS_SECURE_AUTHENTICATION. Если требуется указать конкретного пользователя, необходимо использовать функцию ADsOpenObject (прошу прощения за корявый перевод).

Далее пример использования ADsGetObject для связывания с AD:

interface Uses :. , ActiveDs_TLB; : function ADsGetObject(lpszPathName: WideString; const riid: TGUID; out ppObject: Pointer): HRESULT; stdcall; implementation function ADsGetObject; external 'activeds.dll'; Procedure TForm1.Test Var hr: HResult; objDomain: Pointer; begin hr:= ADsGetObject('LDAP://ou=test, ou=mine, dc=mydomain, dc=com', IID_IADsContainer, objDomain); if Failed(hr) then Exit; end;

Чтобы данный пример мог быть откомпилирован необходимо импортировать библиотеку типов Activeds.tlb, как показано на рисунке 2:


Рисунок 2

Замечание:

При работе с ADsGetObject бывали ситуации, когда при попытке прочитать какое-либо свойство полученного объекта выходила ошибка 'The directory property cannot be found in cache'. К сожалению, это было достаточно давно, и восстановить ситуацию не удалось. Тем не менее ошибка была. Обойти ее удалось при использовании функции ADsOpenObject . Вот пример использования данной функции:

interface Uses :. , ActiveDs_TLB; : function ADsOpenObject(lpszPathName: WideString; lpszUserName: WideString; lpszPassword: WideString; dwReserved: DWORD; const riid: TGUID; out ppObject: Pointer): HRESULT; stdcall; implementation function ADsOpenObject; external 'activeds.dll'; Procedure TForm1.Test Var hr: HResult; objDomain: Pointer; begin hr:= ADsOpenObject('LDAP://ou=test, ou=mine, dc=mydomain, dc=com', '', '', DS_SECURE_AUTHENTICATION, IID_IADsContainer, objDomain);} if Failed(hr) then Exit; end;

Далее в статье будет использоваться только ADsGetObject.

В данных примерах мы пытаемся получить ссылку на интерфейс IID_IADsContainer.

IID_IADsContainer используют для получения коллекции ADSI объектов. Полный список интерфейсов, с которыми можно работать при помощи ADsGetObject, и их описание можно найти в MSDN.

После того, как мы получили ссылку на контейнер, осталось перебрать его объекты и считать их имена. Для этого нам понадобятся еще две функции - AdsBuildEnumerator и ADsEnumerateNext.

AdsBuildEnumerator- создает объект Enumerator (перечеслитель) для конкретного объекта контейнера ADSI.

function ADsBuildEnumerator(pADsContainerL: IADsContainer; ppEnumVariant: PIEnumVARIANT): HRESULT; stdcall; function ADsBuildEnumerator; external 'activeds.dll';

pADsContainerL - указатель на IADsContainer;

ppEnumVariant - указатель на указатель IEnumVariant интерфейс, который связывает создаваемый объект Enumerator с соответствующим объектом контейнером.

Интерфейс IEnumVARIANT описан в модуле ActiveX.

ADsEnumerateNext - позволяет перемещать указатель по элементам коллекции.

function ADsEnumerateNext(pEnumVariant: IEnumVARIANT; cElements: ULONG; pvar: POleVariant; pcElementsFetched: PULONG): HRESULT; stdcall; function ADsEnumerateNext; external 'activeds.dll';

pEnumVariant - получаем после вызова ADsBuildEnumerator;

cElements - количество элементов, которые мы хотим извлечь из коллекции за один раз;

pvar - указатель на массив, в который помещаются извлеченные из коллекции объекты;

pcElementsFetched - указатель на фактическое количество найденных элементов.

Далее, собственно, пример, демонстрирующий как получить список компьютеров домена из AD:

procedure TForm1.Button1Click(Sender: TObject); var objDomain: Pointer; objChild: Pointer; hr: HResult; s: String; i: Integer; iArr : OleVariant; iEnum: IEnumVARIANT; iFetch: ULONG; iAPath: String; begin ListBox1.Clear; hr:= ADsGetObject('LDAP://ou=test, ou=mine, dc=bogatyr, dc=kz', IID_IADsContainer, objDomain); if Failed(hr) then Exit; hr:=ADsBuildEnumerator(IADsContainer(objDomain), @iEnum); if Failed(hr) then Exit; hr := ADsEnumerateNext(iEnum, 1, @iArr, @iFetch); while (S_OK = hr) and (1 = iFetch) do begin hr:=IDispatch(iArr).QueryInterface(IADs,objChild); if Failed(hr) then Exit; if AnsiLowerCase(IAds(objChild).Class_)='computer' then begin s:=IAds(objChild).Name; System.Delete(s,1,3); ListBox1.Items.Add(s); end; if AnsiLowerCase(IAds(objChild).Class_)='organizationalunit' then begin Continue; { s:=IAds(objChild).Name; iAPath:=PAPAth; System.Delete(iAPath, 1, 7); iAPath:='LDAP:// '+s+','+iAPath; if not NextNode_Computer(iAPath) then exit;} end; if AnsiLowerCase(IAds(objChild).Class_)='container' then begin Continue; { s:=IAds(objChild).Name; iAPath:=PAPAth; System.Delete(iAPath, 1, 7); iAPath:='LDAP:// '+s+','+iAPath; if not NextNode_Computer(iAPath) then exit;} end; iArr:=null; hr := ADsEnumerateNext(iEnum, 1, @iArr, @iFetch); end; end;

Часть кода в примере закомментирована. Код взят из рабочей программы и слегка исправлен. В закомментированных частях видно, что подпрограмма вызывается рекурсивно. Это было сделано что бы просканировать всю указанную ветку из AD, включая содержащиеся внутри ветки.

4.2 Смена пароля локального администратора.

Здесь все просто. Формируем строку связывание для доступа к объекту с именем "Администратор". Класс объекта - "user". Объект расположен на рабочей станции "Computer01".

iPath:='WinNT://'+NameWs+'/Администратор,user';

И, собственно, реализация.

procedure ChangePassword; var objUser: Pointer; hr: HResult; iPath: String; i: Integer; begin iPath:='WinNT://Computer01/Администратор,user'; hr:= ADsGetObject(iPath, IID_IADsUser, objUser); if hr<>S_OK then Exit; IADsUser(objUser).SetPassword('anykey'); end;

4.3 Обработка ошибок.

Если вызов ADSI функции завершился неудачей, функция вернет код ошибки стандартным для COM объектов способом. Коды ошибок делятся не четыре группы:

Универсальные коды ошибок COM; Универсальные коды ошибок ADSI; Коды ошибок Win32 для ADSI; Коды ошибок LDAP для ADSI;

Кроме того, некоторые интерфейсы предоставляют дополнительные сведения об ошибке, которые могут быть получены при помощи функции ADsGetLastError.

function ADsGetLastError(lpError: LPDWORD; lpErrorBuf: LPWSTR; dwErrorBufLen: DWORD; lpNameBuf: LPWSTR; dwNameBufLen: DWORD): HRESULT; stdcall;

lpError - указатель на код ошибки;

lpErrorBuf - указатель на буфер, куда будет передано описание ошибки;

dwErrorBufLen - размер буфера;

lpNameBuf - указатель на буфер, куда будет передано имя провайдера, который возбудил эту ошибку;

dwNameBufLen - размер буфера;

Простой пример использования этой функции можно будет посмотреть в исходных кодах, прилагаемых к статье. Для получения наиболее полной информации о произошедших ошибках обратитесь к MSDN. В частности, по приведен пример функций (на Си), которые в качестве аргумента принимают код ошибки и возвращают ее описание. Осталось их (эти функции) перевести на Pascal и использовать.

Список литературы

http://security.software-testing.ru/wiki/Lokal'najaUgroza MSDN "Администрирование Windows с помощью WMI и WMIC" , Андрей Попов и Евгений Шикин.

Исходные коды проекта и пример базы данных (22 K)



VBS


Первая реализация задачи была сделана на VBS. И это понятно. Достаточно зайти на сайт Микрософт и скачать готовые скрипты. И немного их подправить под свои нужды. Кроме того, на VB код получается очень короткий и легкий для восприятия. Вот пример создания списка компьютеров из домена, расположенных в определенном organization unit в Active Directory (AD):

Set objDictionary = CreateObject("Scripting.Dictionary") strDomain = "LDAP://ou=Test, ou=Mine, dc=mydomain, dc=com" Set objDomain = GetObject(strDomain) objDomain.Filter = Array("computer") i = 0 For Each objComputer In objDomain objDictionary.Add i, Mid(objComputer.Name,4) i = i + 1 Next


Рисунок 1

Для получения доступа к пространству имен каталога необходимо связаться с нужным объектом ADSI.

Set objDomain = GetObject(strDomain)

strDomain - строка связывания.

Первая часть строки связывания определяет, к какой именно службе каталогов мы обращаемся.

Примеры обращения к различным службам

"LDAP://"Служба каталогов, созданная на основе протокола LDAP (Active Directory в том числе)
"WinNt://"Служба каталогов в сети Windows NT 4.0 или на рабочей станции Windows XP/2000

Вторая часть строки связывания определяет положение объекта в каталоге.

В следующих таблицах приводятся примеры строк связывания:

LDAP

LDAP:Связь с корнем пространства имен LDAP
LDAP://server01Связь с конкретным сервером
LDAP://server01:390Связь с конкретным сервером через указанный порт
LDAP://CN=Jeff Smith,CN=users,DC=fabrikam,DC=comСвязь с конкретным объектом
LDAP://server01/CN=Jeff Smith,CN=users,DC=fabrikam,DC=comСвязь с конкретным объектом через указанный сервер

WinNT

WinNT://<domain name>
WinNT://<domain name>/<server>
WinNT://<domain name>/<path>
WinNT://<domain name>/<object name>
WinNT://<domain name>/<object name>,<object class>
WinNT://<server>
WinNT://<server>/<object name>
WinNT://<server>/<object name>,<object class>

Устанавливаем фильтр для выделения объектов - компьютеров.

objDomain.Filter = Array("computer")

И затем перебираем элементы коллекции.

Главный минус данной реализации (на мой взгляд) - это низкая скорость работы. Для перебора ~150 рабочих станция и смены на них пароля понадобилось около часа времени.

Основные задержки приходятся на операцию связывания. Особенно большие таймауты при попытке связывания с выключенным или не существующим компьютером ( или если по какой-то причине отказано в доступе). Решением данной проблемы является организация многопоточности. Поэтому от VBS пришлось отказаться.



Создание графического интерфейса пользователя средствами Win32 API


, Королевство Дельфи
09 июля 2003г.

С появлением разнообразных визуальных средств разработки приложений, написание графических интерфейсов программ превратилось в подобие детской игры. Ткнул мышкой - появилась формочка, второй раз ткнул - кнопочка нарисовалась. Как мне кажется, многие сейчас не помышляют об ином способе программирования в графической среде. Безусловно, против прогресса не попрешь, при написании больших проектов все эти удобства очень даже кстати. Но разговор не об этом. Иногда дело доходит до абсурда, примитивное приложение пишется с использованием MFC, VCL etc. Такие программы жрут память, как термиты и занимают, своим жирным телом, лишнее дисковое пространство. Как правило, MFC/VCL аналоги "весят" в десять - двадцать раз больше, чем программы написанные на чистом API. А Visual Basic (да простит меня бог за это словосочетание) с его msvbvmXX.dll? Да и системных ресурсов расходуется значительно больше (в несколько раз). Бедные пользователи, отказывая себе в пиве, копят ассигнации на покупку нового железа. Разве не жалко - бедненьких? Не только же программерам пиво пить? Есть еще один положительный момент в API кодинге, программист становится ближе к операционной системе. Соответственно - лучше ее понимает и контролирует. Да и просто - это очень увлекательное занятие. Повторюсь, все вышесказанное относится именно к маленьким, простеньким программкам, в больших проектах все обстоит совершенно иначе.

Надеюсь, убедил. Поехали.

Мы рассмотрим создание простенького оконного интерфейса с минимальной функциональностью. Это будет простое окошко с двумя полями ввода и двумя кнопочками. При нажатии на кнопку "Copy", текст из первого поля ввода будет скопирован во второе. При нажатии на кнопку "Close", программа завершит свою работу. В дальнейшем оно может послужить шаблоном для написания других, более сложных, приложений. Будем общаться на языке C/C++, хотя и Delphi не обидим. Общий принцип один и тот же, различается только синтаксис. Чтобы работать с системными сообщениями и API-функциями, необходимо к своему проекту подключить заголовочные файлы; в C/C++ это windows.h, в Delphi это модули windows и messages.

Любая программа в ОС Windows состоит из трех основных частей: главной функции, цикла обработки сообщений и оконной функции, которая обрабатывает все сообщения, посылаемые окну.

Наша программа начинает выполняться с функции WinMain(). Это и есть главная функция. Функция WinMain() выполняет, обычно, следующие задачи: Определяет класс окна. Не путать с классом ООП. Регистрирует данный класс в системе. Создает главное окно приложения и другие элементы управления. Отображает окно на экране. Запускает цикл обработки сообщений. Объявляется она вот каким образом: int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) Разберемся с параметрами: hInstance - дескриптор текущего экземпляра приложения. hPrevInstance - дескриптор предыдущего экземпляра приложения, если оно запущено. lpCmdLine - указатель на строку, содержащую параметры передаваемые программе при запуске. nCmdShow - константа определяющая способ отображения окна. (Смотри константы SW_).

В Delphi мы не увидим такой картины, в этой среде разработки главная функция скрывается от программиста компилятором. Хотя, несомненно, она присутствует в конечном коде. Для регистрации класса окна, необходимо заполнить поля структуры типа WNDCLASS (в Delphi TWNDCLASS). У нас, для этого, объявлена переменная wcl. wcl.hInstance = hInstance; Дескриптор текущего экземпляра приложения, переменная hInstance инициализируется функцией WinMain(). В Delphi инициализируется неявным образом. wcl.lpszClassName = szWinName; Имя класса. Строковую переменную szWinName мы создали и инициализировали предварительно. wcl.lpfnWndProc = WindowFunc; Указатель на оконную функцию. wcl.style = 0; Константа, задающая стиль окна. Для этого используется флаги CS_, я просто обнулил. Можно задавать комбинацию флагов с помощью битовой операции "или". wcl.hIcon = LoadIcon(NULL, IDI_ASTERISK); Дескриптор иконки приложения, возвращаемый функцией LoadIcon(). Я загрузил стандартную иконку. Смотри константы IDI_. wcl.hCursor = LoadCursor(NULL,IDC_ARROW); Дескриптор курсора приложения, возвращаемый функцией LoadCursor(). Я загрузил стандартную стрелочку. Смотри константы IDC_. wcl.lpszMenuName = NULL; Указатель на строку, задающую имя ресурса меню для данного оконного класса. Нет меню, нет и указателя. wcl.cbClsExtra = 0; Зарезервированное поле. Обнуляем. wcl.cbWndExtra = 0; Зарезервированное поле. Обнуляем. wcl.hbrBackground = (HBRUSH)COLOR_WINDOW; Цвет окошка. Константа COLOR_WINDOW приводится к типу HBRUSH (в Delphi приводить не нужно). Также, с помощью функции GetStockObject(), можно задать цвет кисти окна или фоновый рисунок. Теперь, смело, регистрируем класс окна. RegisterClass(&wcl); В качестве параметра функции RegisterClass передается указатель на структуру wcl.

Следующей строкой мы создаем наше окно.

hMainWnd = CreateWindow(szWinName, "Простое окно на API.", WS_OVERLAPPEDWINDOW ^ WS_THICKFRAME ^ S_MAXIMIZEBOX, CW_USEDEFAULT, CW_USEDEFAULT, 300, 170, HWND_DESKTOP, NULL, hInstance, NULL);
Первый параметр - имя класса окна. Второй параметр - Заголовок окна. Третий параметр - стиль окна. Из стандартного WS_OVERLAPPEDWINDOW, с помощью операции xor, я изъял возможность масштабирования окна и отключил кнопку максимизации. Четвертый и пятый - положение окна от левого, верхнего угла экрана. У меня CW_USEDEFAULT, при этом значении система выбирает положение окна автоматически. Шестой и седьмой параметры - ширина и высота окна, соответственно. Восьмой параметр - окно владелец. У главного окна, владелец - рабочий стол (0). У элементов управления - главное окно. Девятый - указатель на дескриптор меню. Нет меню, нет и указателя. Десятый параметр - Дескриптор текущего экземпляра приложения. Одиннадцатый - Используется при создании приложений с MDI-интерфейсом. Нам не нужен. Функция возвращает дескриптор созданного окна, который заносится в переменную hMainWnd.
Дескриптор окна - уникальный номер в системе, по которому идентифицируется окно или элемент управления.

Далее мы создадим необходимые элементы управления. Все элементы управления - те же окна, просто они имеют другое имя класса. Классы элементов управления регистрировать не нужно, они уже предопределены в системе. Кнопка - класс button. Поле ввода - класс edit. Надпись - класс ststic. Существует множество классов, которые соответствуют стандартным элементам управления. Контролы создаем с помощью, знакомой нам, функции CreateWindow() и незнакомой CreateWindowEx(). CreateWindowEx() позволяет создать окно с расширенным стилем. Мы используем ее для создания полей ввода. В этой функции добавлен первый параметр, который и задает этот самый расширенный стиль, остальные параметры как у CreateWindow(). Элементы управления являются дочерними окнами, их владелец главное окно.

Создавая контролы, в параметрах функции необходимо указать дескриптор главного окна, а также стиль окна WS_CHILD. Внешним видом и функциональностью элементов управления можно манипулировать с помощью флагов: WS_, ES_, BS_, SS_, объединяя их битовой операцией "или". Создавая контролы, мы инициализируем соответствующие переменные их дескрипторами, которые возвращают функции CreateWindow() и CreateWindowEx(). Эти дескрипторы понадобятся нам для дальнейшей работы с элементами управления. Отображаем, созданное нами, окно на экране и перерисовываем его.

ShowWindow(hMainWnd, nCmdShow); UpdateWindow(hMainWnd); Создаем цикл обработки сообщений. while(GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); }
Функция GetMessage выбирает очередное сообщение из очереди сообщений приложения и отправляет его окну. Первый параметр - структура типа MSG (в Delphi типа TMSG) Второй параметр - дескриптор окна, которому предназначено сообщение. Если NULL или 0, то все окна приложения. Третий и четвертый - позволяют задать диапазон принимаемых сообщений. Если 0, то все сообщения, адресованные окну. GetMessage - возвращает FALSE при появлении сообщения WM_QUIT, в этом случае происходит выход из цикла и приложение завершает работу. TranslateMessage - переводит виртуальные коды клавиш в клавиатурные сообщения. DispatchMessage - отправляет сообщение оконной функции, для обработки.

Оконная функция обеспечивает функциональность программы, путем обработки системных сообщений. Оконная функция является CALLBACK - функцией, т.е. вызывается операционной системой в ответ на поступившее, новое сообщение. Оконная функция объявлена таким образом: LRESULT CALLBACK WindowFunc(HWND hMainWnd, UINT iMsg, WPARAM wParam, LPARAM lParam) HMainWnd - дескриптор главного окна. iMsg - номер сообщения. Смотри константы WM_. lParam и wParam - параметры сообщения.

При появлении сообщения, мы можем сравнить параметр iMsg с одной из констант WM_ и запрограммировать соответствующую реакцию программы.

Например: при нажатии левой кнопки мыши, когда указатель мыши находится над клиентской областью окна, возникает событие WM_LBUTTONDOWN. Вызывается оконная функция, в параметр iMsg заносится значение константы WM_LBUTTONDOWN, мы можем проверить условие и запрограммировать нужную нам реакцию программы.

Внутри оконной функции расположен оператор выбора, который и выполняет вышеописанную задачу. В операторе выбора обязательно должен быть организован обработчик по умолчанию, который реализуется функцией DefWindowProc(hMainWnd, iMsg, wParam, lParam);

Если этого не сделать, наша программа издохнет так и не ожив. Множество сообщений, обрабатывается самой системой, такие как: изменение размеров окна, сворачивание/разворачивание окна, вызов системного меню etc. Для этого и служит DefWindowProc().

При работе с оконными элементами управления, окну владельцу посылается сообщение WM_COMMAND, при этом lParam содержит дескриптор элемента управления, а старший байт параметра wParam - идентификатор события, вызванного в элементе управления. Например: при нажатии на кнопку - BN_CLICKED. Смотри константы BN_, WM_. Закрыть прогу мы можем использовав функцию PostQuitMessage(0). Эта функция посылает окну сообщение WM_QUIT.

Несколько слов о том, как писать такие программы на Delphi. Создаем новый проект, запускаем Project Manager, удаляем Unit1 вместе с формой. Жмем Ctrl + F12 и открываем файл проекта. Удаляем из uses модуль forms, добавляем туда windows и messages. Удаляем все между begin и end. Заготовка готова. Можно кодить. Писать программы на чистом API невозможно без справки, которая всегда должна быть под рукой. Будь ты самим Гейтсом - все не запомнить. Рекомендую: прежде всего - ; справочная система Delphi (файл MSTOOLS.HLP); на сайте есть русская справка по Win32 API. Вот и все.
Удачи.

Скачать: (2.6 K)
архив содержит файлы windows.cpp и windows.dpr



Дельфийское слово


Антончук Сергей,

Cамая прогрессивная часть человечества - программисты - вынуждены зачастую заниматься изобретением велосипеда. Почему это происходит? В большинстве случаев, реализуемый программистом алгоритм уже присутствует в компьютере пользователя, и даже неоднократно. Но воспользоваться им из новой программы нет возможности, или нет информации, как это делается. Речь пойдет о Delphi-Word.

Наглядный пример— MS Office. Зачем самому создавать систему генерации отчетов, деловых диаграмм или алгоритм линейной оптимизации. Все уже есть в MS Office, кроме того, есть механизмы использования всего этого богатства, называемые OLE Automation. Только в Help нужно заглянуть… И тут выступает проза жизни. Написать самому оказывается быстрее, чем найти в колоссальной по объему системе помощи нужную информацию. Кроме того, тут даже есть элемент комизма. Нужный вам раздел системы помощи при обычной установке, как правило, не инсталлируется. В общем, кладезь мудрости, засыпанный второстепенной информацией и к тому же, лежащий на дистрибутивном диске. А если очень хочется, или постановка задачи явно требует? Приведенный далее материал— попытка дать «быстрый старт» программисту, на которого взвалили такой выгодный заказ. Речь пойдет о «связке» Delphi-Word, однако, многое из ранее изложенного применимо ко многим другим приложениям Microsoft (Excel, Internet Explorer и т. д.), которые поддерживают OLE Automation.



Форматирование текста


var
S: Selection;
...
S := Word.Selection;

{вывод фразы полужирным шрифтом}
S.Font.Bold := integer(True);
S.TypeText('Be bold!');
S.Font.Bold := integer(False);
S.TypeParagraph;
{прописным шрифтом}
S.Font.Italic := integer(True);
S.TypeText('Be daring!');
S.Font.Italic := integer(False);

Особых пояснений не требуется, единственно, что следует помнить о свойстве «Application.Options.ReplaceSelection», чтобы новый текст случайно не затер старый.



Как подключиться к загруженной копии Word


Для подключения к исполняемой копии Word можно использовать команду «GetActiveOleObject». Она возвращает переменную с именем «IDispatch», содержащую указатель на работающее приложение Word. После этого у него можно с помощью «QueryInterface» запросить указатель на объект «_Application». GetActiveOleObject генерирует исключение, если запрашиваемый объект не существует в Running Object Table (ROT). Поэтому вызов его необходимо делать внутри блока «try..except».

uses Word_TLB;

procedure StartWord(var WordApp: _Application);

var
SaveChanges: OleVariant;
begin
try
GetActiveOleObject('Word.Application').QueryInterface(_Application, WordApp);
except
WordApp := nil;
end
if (UnAssigned(WordApp)) then
try
WordApp := CoApplication.Create;
WordApp.Visible := True;
except
if (Assigned(WordApp)) then begin
SaveChanges := wdDoNotSaveChanges;
WordApp.Quit(SaveChanges, EmptyParam, EmptyParam);
end;
end;
end;



Открытие существующего документа


В Word 97:

var
FileName: OleVariant;
begin
FileName := 'C:\My Documents\Открываемый файл.doc';
Word.Documents.Open(FileName, EmptyParam, EmptyParam,
EmptyParam, EmptyParam, EmptyParam, EmptyParam,
EmptyParam, EmptyParam, EmptyParam);

Необязательные параметры задают следующие свойства:

ReadOnly (третий параметр, по умолчанию False);
PasswordDocument (пятый параметр);
Format (последний параметр дает возможность выбрать конвертор для файла).

В Word 2000 метод «Documents.Open» имеет два дополнительных параметра, для расшифровки и определения,— должен ли документ быть видимым на экране. Как и в случае с методом «Add» при использовании его в Word 97 вызывается исключение, поэтому для совместимости нужно использовать метод «Documents.OpenOld». Он имеет такие же параметры, как и метод «Open» из Word.



Открытие Word через OLE Automation


В библиотеке типов определен касс «CoApplication», который реализует интерфейс с Word-ом. Для создания из своей программы экземпляра Word необходимо вызвать метод «CoApplication.Create». Этот метод возвращает ссылку на интерфейс типа «_Application». Интерфейс «_Application» предоставляет интерфейс «Documents», в котором определены два метода доступа к документам: «Add» и «Open».

Оба эти метода возвращают указатель на интерфейс «_Document». При вызове методам можно передавать параметры «OLEVariant». Многие параметры, передаваемые методам Word, являются необязательными (optional). Необязательные параметры, тем не менее, должны быть включены в обращения к методам, но могут быть определены как неинициализированные (Unassigned), чтобы указать, что они не используются. Для неиспользуемых параметров можно использовать переменную Delphi 4 называемую EmptyParam.

uses Word_TLB;

procedure StartWord(var WordApp: _Application; var WordDoc: _Document);

var
SaveChanges: OleVariant;
begin
try
WordApp := CoApplication.Create;
WordDoc := WordApp.Documents.Add(EmptyParam, EmptyParam);
WordApp.Visible := True;
except
if (Assigned(WordApp)) then begin
SaveChanges := wdDoNotSaveChanges;
WordApp.Quit(SaveChanges, EmptyParam, EmptyParam);
end;
end;



Переход к закладке по имени


var
What : OLEVariant;
Name : OLEVariant;
begin
What := wdGoToBookmark;
Name := 'Имя закладки';
App.Selection.GoTo_(What, EmptyParam, EmptyParam, Name);

Очень полезный метод. Позволяет превратить Word в генератор писем или отчетов. В заготовке письма проставляем закладки, затем из программы переходим к ним и вставляем нужный текст, например из базы данных. Второй необязательный параметр— счетчик. С помощью него можно организовать переход не по имени, а по номеру закладки.



Получение информации из Word


Если кто-то не помнит — Word когда-то был текстовым процессором, поэтому из уважения к его истории в нем эти функции все еще присутствуют. Информацию из Word можно получить через интерфейс «IdataObject». Для получения указателя на этот интерфейс необходимо использовать функцию «QueryInterface».

Документы Word поддерживают стандартные форматы CF_TEXT и CF_METAFILEPICT так же как ряд других специфических форматов, включая RTF и structured storage. Для стандартных форматов используются константы значений переменной «cfFormat», но для других форматов нужно делать запрос, используя функцию «EnumFormatEtc». Эта функция возвратит список обеспечиваемых форматов. Требуемый формат из этого списка затем передается функции «GetData» интерфейса «IDataObject». Значение cfFormat для одинаковых форматов может различаться на разных компьютерах, поэтому всегда должно находиться с помощью функции «EnumFormatEtc». Для подробной информации относительно методов интерфейса «IdataObject» можно обратиться к файлам помощи по программированию в Win32.

uses Word_TLB;

function GetRTFFormat(DataObject: IDataObject; var RTFFormat: TFormatEtc):

Boolean;
var
Formats: IEnumFORMATETC;
TempFormat: TFormatEtc;
cfRTF: LongWord;
Found: Boolean;
begin
try
OleCheck(DataObject.EnumFormatEtc(DATADIR_GET, Formats));
cfRTF := RegisterClipboardFormat('Rich Text Format');
Found := False;
while (not Found) and (Formats.Next(1, TempFormat, nil) = S_OK) do
if (TempFormat.cfFormat = cfRTF) then begin
RTFFormat := TempFormat;
Found := True;
end;
Result := Found;
except
Result := False;
end;
end;
procedure GetRTF(WordDoc: _Document);
var
DataObject: IDataObject;
RTFFormat: TFormatEtc;
ReturnData: TStgMedium;
Buffer: PChar;
begin
if (Assigned(WordDoc)) then try
WordDoc.QueryInterface(IDataObject, DataObject);
if GetRTFFormat(DataObject, RTFFormat) then begin
OleCheck(DataObject.GetData(RTFFormat, ReturnData));
//RTF is passed through global memory
Buffer := GlobalLock(ReturnData.hglobal);
//Buffer is a pointer to the RTF text
//Insert code here to handle the RTF text (ie. save it, display it etc.)
GlobalUnlock(ReturnData.hglobal);
end;
except
ShowMessage('Error while getting RTF');
end;
end;



Создание и доступ к таблицам


Создать таблицу можно следующим образом (проверялось в Word 97):

var
Doc: _Document;
T: Table;
begin
Doc := Word.ActiveDocument;
T := Doc.Tables.Add(Word.Selection.Range, 5, 3);
T.Cell(1, 1).Range.Text := 'January';
T.Cell(1, 2).Range.Text := 'February';
T.Cell(1, 3).Range.Text := 'March';
T.Columns.Width := 72; // in points

Получение содержимого ячейки происходит так:

Caption := T.Cell(1, 3).Range.Text;

Учтите, что работа с таблицами в Word происходит очень медленно, а в Word 2000 чрезвычайно медленно. Поэтому, если в результирующем документе просто нужно быстро поместить таблицу, то можно, например, разместить текст, разделенный запятыми (или другим разделителем), а затем преобразовать его в таблицу. Сделать это можно так:

const
Line1 = 'January,February,March';
Line2 = '31,28,31';
Line3 = '31,59,90';
var
R: Range;
Direction, Separator, Format: OleVariant;
begin
Doc := Word.ActiveDocument;
R := Word.Selection.Range;
Direction := wdCollapseEnd;
R.Collapse(Direction);
R.InsertAfter(Line1);
R.InsertParagraphAfter;
R.InsertAfter(Line2);
R.InsertParagraphAfter;
R.InsertAfter(Line3);
R.InsertParagraphAfter;
Separator := ',';
Format := wdTableFormatGrid1;
R.ConvertToTable(Separator, EmptyParam, EmptyParam,
EmptyParam, Format, EmptyParam,
EmptyParam, EmptyParam, EmptyParam,
EmptyParam, EmptyParam, EmptyParam,
EmptyParam, EmptyParam);



Создание нового документа


В Word 97 на основании шаблона «Normal»:

Word.Documents.Add(EmptyParam, EmptyParam);

Если Вы хотите, чтобы новый документ был основан на шаблоне отличном от «Normal», передайте имя (и путь) шаблона как первый параметр. Если необходимо открыть новый документ как шаблон, передайте «True» для второго параметра.

В Word 2000, метод «Documents.Add» имеет два дополнительных параметра, для типа документа и определения, должен ли документ быть видимым на экране. Внимание! Использование этого метода с Word 97 вызовет исключение. Если необходима совместимость с Word 97, можно использовать метод «Documents.AddOld» из библиотеки Word 2000. Он имеет такие же параметры, как и метод «Add» из Word 97.



Установка Delphi для работы с Word


Для того чтобы из Delphi можно было обращаться к методам и свойствам, предоставляемым Word (используя раннее связывание OLE Automation), необходимо установить библиотеку типов Word. Библиотека типов объявляет в стандартизированном виде все методы и свойства Automation Server, которые могут быть использованы любым совместимым средством программирования, включая Delphi. Для того чтобы использовать в Delphi библиотеку типов Word в меню «Project» -> «Import Type Library» необходимо выбрать файл msword8.olb, находящийся в каталоге Microsoft Office и подкаталоге «Office». При этом будет создан файл «Word_TLB.pas», в котором содержимое библиотеки типов представлено на языке object pascal. Будут созданы также файлы библиотек «Office_TLB.pas» и «VBIDE_TLB.pas», на которые ссылается библиотека типов. Эти файлы необходимо сохранить в каталоге «Imports». Теперь достаточно в секцию «uses» нового модуля добавить «Word_TLB» для работы с Word посредством OLE Automation.

Подробную информацию о предоставляемых приложениями Office методах и свойствах можно найти в файлах vba*.hlp. Учтите только, что по умолчанию они не устанавливаются. Для их установки для каждой программы необходимо явно указать наличие файлов справки по Visual Basic или выполнить полную установку.

Следует отметить также, что в Delphi версии 5 есть стандартные методы для работы в Word. Это модули Word97 и Word2000, которые уже содержат в себе библиотеку типов Word.



Вставка текста


var
S: Selection;
...
S :=Word.Selection;
S.TypeText(Вставляемый текст');
S.TypeParagraph;
S.TypeParagraph;
S.TypeText('Текст после пустой строки');

Если выставить свойство «Application.Options.ReplaceSelection» в «True», выделенный текст будет заменяться новым.



Приведенная ранее информация позволит вам


Приведенная ранее информация позволит вам передавать в Word информацию и генерировать или заменять документы. Существует также возможность получать из Word некоторые события для их обработки, но они позволяют только отследить открытие/закрытие документов, самого Word и активацию OCX-элементов. Поэтому не думаю, что вам это пригодится, хотя, если народ пожелает…


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
Внимание! Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.
Приобретайте новую Ansher, только здесь Вам предложат самые низкие цены и высокий уровень сервиса.



Закрытие документа


var
SaveChs: olevariant;
begin
SaveChs := wdSaveChanges;
Word.ActiveDocument.Close(SaveChs, EmptyParam, EmptyParam);

Параметры имеют те же значения, что и в случае метода «Word.Close», с одним неприятным исключением: при передаче значения «wdPromptToSaveChanges» метод останавливается. Поэтому запрос пользователю нужно сделать самостоятельно.



Закрытие Word


Быстрое закрытие без сохранения изменений приведено далее:

var
SaveChanges: OleVariant;
begin
SaveChanges := wdDoNotSaveChanges;
Word.Quit(SaveChanges, EmptyParam, EmptyParam);

Другие возможные варианты для параметра «SaveChanges» — это «wdSaveChanges» (сохранить изменения) и «wdPromptToSaveChanges» (запросить у пользователя необходимость сохранения изменений).

Второй параметр используется для документов не Word формата. Возможные значения «wdOriginalDocumentFormat» (сохранить в исходном формате), «wdPromptUser» (запросить формат у пользователя) или «wdWordDocument» (сохранить как документ Word).

The last parameter should be set to True if you want the document to be routed to the next recipient in line.

Последний параметр должен быть установлен в «True», если вы хотите, чтобы документ был направлен следующему получателю по маршруту.



Class property


Раньше, хотя компилятор и позволял использовать методы класса в качестве аксессоров свойств, обращение к таким свойствам в форме TSomeClass.PropName было невозможно. Теперь, с введением свойств класса такое обращение разрешено. Однако, в отличие от Delphi for .NET, свойства класса могут работать только через методы, т.к. понятие полей класса для компилятора Delphi for Win32 отсутствует.

type TTestClass = class class function GetClassProp: integer; class procedure SetClassProp(value: integer); class property ClassProp: integer read GetClassProp write SetClassProp; end; TestClass.ClassProp := ...;



Error Insight


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

Например, самая обычная ошибка — неверно написан идентификатор или не подключен модуль, в котором он определен. Если раньше, до момента компиляции эти ошибки не отслеживались, то сейчас ошибочные имена (идентификаторов, методов и т.п.) мгновенно обращают на себя внимание, так как в редакторе кода они подчеркиваются красной волнистой линией, а в окне "Structure" появляется полный список таких ошибок (см. ). Естественно, что, кликнув на ошибке в списке, мы сразу попадаем на нужное место в коде.



For..in..do


В язык Delphi добавлена конструкция for..in..do для перебора всех членов массива, строки, множества или коллекции.

for Element in ArrayExpr do Stmt; for Element in StringExpr do Stmt; for Element in SetExpr do Stmt; for Element in CollectionExpr do Stmt;

То есть, в цикле перебираются все элементы некоего множества и для каждого выполняется тело цикла. Например, теперь ничего не стоит перевернуть строку наоборот (см. )

Полная и весьма понятная информация находится в справке, смотрите раздел "Declarations and Statements"

Наиболее интересным применением циклов такого вида является их использование с коллекциями для перебора элементов. Для того, чтобы класс можно было использовать с циклами for..in, класс должен реализовать предопределенный паттерн коллекции. А именно, класс должен удовлетворять следующим требованиям:

содержать public метод экземпляра с именем GetEnumerator, который должен возвращать экземпляр класса, ссылку на интерфейс или запись (record). экземпляр класса, ссылка на интерфейс или запись, возвращенные методом GetEnumerator, должны содержать public метод экземпляра с именем MoveNext, возвращающий значение типа boolean. экземпляр класса, ссылка на интерфейс или запись, возвращенные методом GetEnumerator должны содержать public свойство экземпляра с именем Current, тип которого должен соответствовать типу элементов контейнера.

Экземпляр, возвращенный методом GetEnumerator, автоматически разрушается после окончания цикла for..in.

Следующий пример показывает реализацию паттерна коллекции

program Project1; {$APPTYPE CONSOLE} type TMyIntArray = array of Integer; TMyEnumerator = class Values: TMyIntArray; Index: Integer; public constructor Create; function GetCurrent: Integer; function MoveNext: Boolean; property Current: Integer read GetCurrent; end; TMyContainer = class public function GetEnumerator: TMyEnumerator; end; constructor TMyEnumerator.Create; begin inherited Create; Values := TMyIntArray.Create(100, 200, 300); Index := -1; end; function TMyEnumerator.MoveNext: Boolean; begin if Index < High(Values) then begin Inc(Index); Result := True; end else Result := False; end; function TMyEnumerator.GetCurrent: Integer; begin Result := Values[Index]; end; function TMyContainer.GetEnumerator: TMyEnumerator; begin Result := TMyEnumerator.Create; end; var MyContainer: TMyContainer; I: Integer; Counter: Integer; ar: TMyIntArray; begin MyContainer := TMyContainer.Create; Counter := 0; for I in MyContainer do Inc(Counter, I); WriteLn('Counter = ', Counter); end.

Поддержка синтаксиса for...in уже встроена в ряд классов VCL, например, TList, TComponent, TCollection, и т.д. - в общей сложности около 15 классов. Так, например, перечисление имен компонентов формы может выглядеть следующим образом (хотя и непривычно):

var I: TComponent; begin for I in Self do ListBox.Add (I.Name); end;



Help Insight


Если в привычном "Code Insight" показывался тип идентификатора (переменной, функции и т.д.) и модуль, в котором он определен, то "Help Insight" представляет собой всплывающее окно-подсказку, с кратким описанием этого идентификатора и дополнительными ссылками (см. рис). Достаточно подвести мышку к нужному идентификатору, чтобы получить такой "маленький help". Использовать "Help Insight" можно в комбинации с "Code Completion". Если в окне "Code Completion" выбрать определенное свойство или метод, то справа появится окно с подсказкой.

Открыть скриншот в отдельном окне

Эта возможность реализована не только для стандартных, но и для собственных классов и переменных. Использование "Help Insight" включено по умолчанию. Местонахождение в настройках: Tools->Options->Editor Options->Code Insight



Хранимые процедуры


Доступные операции Refresh/View Parameters. Окно просмотра параметров открывается при двойном клике на имени процедуры. В этом окне можно указать значения всех входных параметров и, выполнив процедуру, получить заполненные значениями выходные параметры (см. рис). Для выполнения процедуры можно воспользоваться иконкой в верхнем левом углу окна или по правой кнопке мыши (команда Execute).

Если хранимая процедура в качестве результата возвращает еще и (или только) набор данных, необходимо поставить галочку "Stored procedure has one or more cursors" и выполнить процедуру снова.

Просмотр скриншотов в отдельном окне:

Окно просмотра параметров

Окно с настройкой параметров процедуры и получением результатов

Результат работы хранимой процедуры, которая возвращает набор данных

Безусловно, новые возможности для тестирования работы хранимых процедур очень удобны и позволят упростить жизнь разработчика баз данных.

В качестве недостатка можно отметить отсутствие возможности прервать выполнение хранимой процедуры, если время выполнения оказывается довольно продолжительным.



Inline


В новой версии компилятора Delphi появилась возможность использования inline кода. Для этого наполнена новым смыслом директива inline. Теперь, если процедура или функция имеют директиву inline, это заставит компилятор попытаться вставить в место, где эта процедура используется, не вызов, а код тела процедуры. Само наличие директивы inline ещё не гарантирует того, что попытка будет удачной. Существуют определенные ограничения. Согласно справочной системе, эти ограничения таковы:

inline не работает для любых видов методов позднего связывания (virtual, dynamic, message) inline не работает для процедур содержащих код на языке ассемблера, inline не работает для конструкторов и деструкторов inline не работает для главного блока программы и секций инициализации и финализации модулей inline код может быть использован внутри пакетов, но inline не работает через границы пакетов inline не работает в модулях, связанных кольцевой зависимостью. Это ограничение включает и неявные кольцевые ссылки между модулями. Например, если модуль A использует модуль B, модуль B использует C, а C, в свою очередь, использует A, то при компиляции модуля A не будет производиться inline-подстановка кода из модулей B и C. inline-подстановка в модулях, входящих в кольцевые зависимости, может быть произведена, если подстановка производится из модуля, не входящего в кольцо зависимостей. Например, если в предыдущем примере модуль A использует также модуль D, то в модуле A возможна inline-подстановка кода из модуля D. inline не работает, если процедура объявлена в секции interface модуля и обращается к символам, объявленным в секции implementation. inline не работает для методов в классах, если они обращаются к членам классов, имеющим более низкую видимость, чем сам метод. Например, если public метод обращается к private методу, то для такого метода inline-подстановка осуществляться не будет. если процедура, помеченная как inline использует процедуры или переменные из внешних модулей, то все эти модули должны быть перечислены в списке uses того модуля, где inline процедура будет использована, иначе inline-подстановка не производится. inline-подстановка не осуществляется для процедур и функций, которые используются в выражениях проверки условия циклов while и repeat.

При использовании inline процедур следует помнить о том, что изменение кода такой процедуры приведет к перекомпиляции всех модулей, где она используется.

Также для контроля использования inline-подстановок введена директива компилятора {$INLINE}. Она может принимать следующие значения:

{$INLINE ON} - Процедуры, с директивой inline будут помечены, как процедуры, для которых возможна inline-подстановка. При вызове таких процедур будет сделана попытка inline подстановки кода. Действует по умолчанию. {$INLINE AUTO} - Процедуры, с директивой inline будут помечены, как процедуры, для которых возможна inline-подстановка только в том случае, если код процедуры будет размером меньше 32 байт. {$INLINE OFF} - Процедуры, не смотря на наличие директивы inline, никогда не будут помечены, как процедуры, для которых возможна inline-подстановка. При вызове процедур попытка inline подстановки кода не будет сделана.

История изменений


По мере редактирования и сохранения кода в проекте, средой автоматически ведется история этих изменений. Для каждого модуля, включенного в проект, можно просмотреть историю изменений, сравнить разные версии файлов и, если необходимо, возвратиться к любому из промежуточных изменений кода проекта. Смотрите закладку History в редакторе кода и тему "History Manager" в документации.

Открыть скриншот в отдельном окне

После даже этой небольшой работы с новой IDE, возвращение к старым версиям в свои рабочие проекты кажется откатом назад.



Комментирование блока текста


Начнем с одной приятной мелочи. Не знаю, как кому, но мне ее иногда очень не хватало в Delphi. Я имею в виду возможность быстро закомментировать или раскомментировать блок текста в редакторе.

В новой IDE это делается легким движением руки. Выделяем текст, нажимаем клавиши [Ctrl+/] и весь выделенный код оказался закомментирован. Обратная операция делается точно так же. На всякий случай напомню, почему это лучше, чем обычные скобки {} в начале и конце куска кода. В случае использования в начале каждой строки двойного слеша нет никакой нужды заботиться о вложенных комментариях, которые могут уже быть в выделенном тексте. Этот способ "устранения" части кода бывает удобен при отладке.

Очень удобна новая возможность редактора показывать соответствующие пары скобок (см. рисунок).



Компилятор


Предопределенный символ для идентификации компилятора - VER170 (выяснено опытным путем, в Help информация отсутствует).

Многие из перечисленных ниже возможностей являются нововведениями только для компилятора Win32.



Модификаторы области видимости


Для большей совместимости исходного кода с Delphi for .NET введены два новых модификатора области видимости членов класса.

strict private

Члены класса с видимостью strict private доступны только самому классу.

strict protected

Члены класса с видимостью strict protected доступны только самому классу и его непосредственным наследникам.

Отличие новых модификаторов от традиционных private и protected заключается в том, что члены с новыми модификаторами не доступны постороннему коду, находящемуся в том же модуле.



Ненаследуемые классы


Если по каким-то причинам разработчик хочет запретить создание наследников класса, это можно сделать используя модификатор sealed.

type TMyFinalClass = class sealed(TObject) end;

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

Например, компиляция кода

type TClass1 = class private .... public constructor Create; destructor Destroy; override; final; .... end; TClass2 = class(TClass1) .... public destructor Destroy; override; end;

приведет к ошибке - E2352 Cannot override a final method.



Но, есть и неприятные моменты.


Русские буквы в комментариях к коду

Поначалу неприятно удивило открытие файлов кода с комментариями в заголовке, написанными русскими буквами в кодировке Win1251. Часть таких файлов открываются, как двоичные. После небольшого исследования оказалось, что портит все маленькая буква "я" в тексте комментариев в начале модуля. Если в новой среде написать такой комментарий в начале модуля, то он редактируется нормально. Но, если его закрыть, то вновь откроется он в двоичном виде. По-видимому, проблема связана с тем, что редактор кода по первой порции фиксированного объема определяет формат файла. Встречая в этой порции букву "я" (ее код $FF), редактор некорректно определяет формат файла. При переносе текста с буквой "я" в конец файла или в середину файла большого размера, его формат определяется корректно.

Сходная ситуация обсуждалась в .

Проблема с открытием таких файлов решается или удалением слов из заголовка модуля, содержащих маленькую букву "я", или замена ее на букву "Я" в верхнем регистре. Кому что нравится.

Русские буквы в названиях каталогов проекта

Проекты VCL.NET и WinForms не запускаются из-под среды, если в полном имени каталога проекта есть русские буквы. Среда сообщает "Unable to create process".

К сожалению, ссылки в окне "Help Insight" не будут работать, если у вас в названии каталогов используются русские буквы.



Отладка


Понравилось поведение системы при возникновении Exception — появляется окно с уведомлением об Exception и с возможностью поставить галочку "Игнорировать этот тип Exception", вместо того, чтобы открывать окно Tools | Debugger..

Изменилась работа с точками останова. Появилась очень удобная возможность, не удаляя точку останова, отключить ее, пометив как "disable". Это можно сделать в редакторе кода по правой кнопке на точке останова, и прямо в списке "Breakpoint list". В этом списке можно включать/выключать все точки или определенные группы (меню по правой кнопке). Так же, теперь прямо в окне "Breakpoint list" можно изменять значение Condition и принадлежность к определенной группе для каждой точки.



Первая страница


После загрузки среды перед нами возникает приветственная страница "Welcome Page". Это новый аналог того самого окошка новостей, которое выглядело довольно неказисто, если ваш компьютер не был подключен к интернету во время работы. Однако не стоит так же сбрасывать со счетов и новый вариант. Кроме ленты новостей (RSS), "Welcome Page" содержит немало полезных ссылок. Во-первых, в самом верху страницы перечислен список проектов, которые вы уже открывали некоторое время назад, с указанием даты последней модификации. Каждая ссылка, естественно, открывает выбранный проект.

Показать скриншот в отдельном окне

В левой части страницы подобраны ссылки на справочную информацию, документацию, которая устанавливается на ваш компьютер при инсталяции Delphi2005. Далее идут ссылки в интернет, на страницы компаний-разработчиков, чьи продукты встроены в среду. Например, на страницу Rave Reports, IntraWeb и так далее. И, наконец, ссылки на новостные группы в интернете, BDN и другие страницы Borland-ресурсов. То есть, "Welcome Page", действительно содержит полезную информацию. Может быть, не часто придется ею пользоваться, но и забывать о ней не стоит, может пригодиться.

Среда предоставляет возможнсть работать как в классическом стиле предыдущих версий Delphi (с отдельными окнами для дизайнера форм, инспектора объектов и т.д.), так и в стиле, максимально приближенном к MS Visual Studio, когда все окна пристыковываются к центральному, в котором можно заниматься дизайном формы или редактированием кода.

Для того, чтобы привести окна редактирования и палитры компонентов к привычному виду, поэкспериментируйте с настройками Tools | Environment Options | Delphi Options | VCL Designer | Embedded designer и Tools | Environment Options | Tool Pallete.

В Delphi2005 изменилась справочная система. Изменился не только внешний вид, но внутренняя структура предлагаемой информации.

На мой взгляд, пользоваться справкой стало намного удобнее.

Показать скриншот в отдельном окне

Появилась возможность настраивать цвета для Object Inspector, смотритеTools | Environment Options | Object Inspector.



Расширенный синтаксис объявления и инициализации массивов


Delphi для Win32

Теперь можно делать задание размеров массива и инициализацию одной строкой

type TMyIntArray = array of Integer; var Ints: TMyIntArray; begin Ints := TMyIntArray.Create(1,2,3,4,5);

Delphi for .NET

Новый расширенный синтаксис позволяет объявлять массивы в форме

array[, ..., ] of baseType;

Также возможна инициализация массивов при помощи стандартной процедуры new.

var a: array [,,] of integer; // 3 dimensional array b: array [,] of integer; // 2 dimensional array c: array [,] of TPoint; // 2 dimensional array of TPoint begin // New taking element type and size of each dimension. a := New(array[3,5,7] of integer); // New taking the element type and initializer list. b := New(array[,] of integer, ((1,2,3), (4,5,6))); // New taking an initializer list of TPoint. c := New(array[,] of TPoint, (((X:1;Y:2), (X:3;Y:4)), ((X:5;Y:6), (X:7;Y:8)))); end.

Рефакторинг


Рефакторинг "переименование символа" — при позиционировании курсора на нужном идентификаторе и выборе пункта меню Refactoring | Rename Field, среда показывает все строки, где встречается выбранный идентификатор, и предлагает выбрать новое имя. Очень удобная возможность, как тут не вспомнить Мартина Фаулера:

"Важной частью пропагандируемого мною стиля программирования является разложение сложных процедур на небольшие методы. Если делать это неправильно, то придется изрядно помучиться, выясняя, что же делают эти маленькие методы. Избежать таких мучений помогает назначение методам хороших имен. Методам следует давать имена, раскрывающие их назначение. Хороший способ для этого - представить себе, каким должен быть комментарий к методу, и преобразовать этот комментарий в имя метода. Жизнь такова, что удачное имя может не сразу придти в голову. В подобной ситуации может возникнуть соблазн бросить это занятие - в конце концов, не в имени счастье. Это вас соблазняет бес, не слушайте его. Если вы видите, что у метода плохое имя, обязательно измените его. Помните, что ваш код в первую очередь предназначен человеку, а только потом - компьютеру. Человеку нужны хорошие имена. Вспомните, сколько времени вы потратили, пытаясь что-то сделать, и насколько проще было бы, окажись у пары методов более удачные имена. Создание хороших имен - это мастерство, требующее практики; совершенствование этого мастерства - ключ к превращению в действительно искусного программиста. То же справедливо и в отношении других элементов сигнатуры метода. Если переупорядочивание параметров проясняет суть - выполните его."

Раньше для подобных действий использовался метод переименования идентификатора в месте, где он объявлен, и инкрементная компиляция до выяснения всех мест, в которых этот идентификатор использовался.

Рефакторинг "Выделение метода" оказался вторым из удобных нововведений, в выделенном методе автоматически объявляются необходимые параметры и локальные переменные. К сожалению, пока остается мечтой автоматический поиск аналогичного кода выделенного метода в других местах и замена его вызововом выделенного метода.

Рефакторинг: процесс выделения метода

После выделения метода

Остальные рефакторинги (объявить переменную, объявить поле, выделить ресурсную строку), на мой взгляд, являются не рефакторингами в классическом смысле, а удобной возможностью редактора кода вставлять необходимые объявления, не перемещаясь в раздел объявлений.



SQL Window


Это привычное окно для выполнения SQL-запросов. Так как тестирование проходило только для MS SQL Server, то возможно, некоторые странности связаны с конкретным драйвером.

Сами SQL-запросы любой сложности (UNION, вложенные подзапросы и т.п.) выполняются без проблем. Странности начались после попытки исполнить в этом окне процедуру ("execute имя_процедуры"), которая в качестве результата возвращает набор данных. В качестве результата было получено сообщение "-1 row(s) affected". И этот результат был одинаков для всех процедур одного сервера. Тест на другом сервере дал иной результат, возможно этот эффект зависит от настроек на сервере (или от настроек конкретной базы), но такого иследования не проводилось. Итак, на другом сервере после выполнения процедуры было получено окошко с сообщением, например, таким: "192 row(s) affected", что само по себе верно, но никакого результата, то есть набора данных, все равно не было выведено. Если в тексте процедуры был оператор "Insert Into имя_таблицы exec имя_процедуры", то в качестве nколичества обработанных строк в результирующем сообщении выдавалось количество строк этого insert'а, а вовсе не nпоследнего select'а процедуры.

Можно предположить, что проблема кроется в ADO.NET, на котором реализован Data Explorer.

И, все же, несмотря на это, Data Explorer добавляет в среду разработки функционал, который реально может облегчить жизнь разработчику клиентских приложений для баз данных.

* * *

Данный обзор составлен по результатам первого знакомства с новой средой разработки. Изучить все нововведения в Delphi2005 за короткий срок невозможно, но такой задачи мы и не ставили перед собой. Разделив работу на параллельные части, мы постарались охватить как можно больше интересных, с нашей точки зрения, нововведений и "вкусностей" нашей любимой среды разработки.

Спасибо компании за возможность ознакомиться с новой версией, которая несомненно, позволит вывести разработку приложений на качественно новый этап.

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



Сворачивание части кода


Как и в Delphi8, в редакторе Delphi2005 реализовано частичное скрытие (сворачивание кода). Это позволяет работать с большими текстами, не прокручивая многостраничный экран. Достаточно оставить развернутым сейчас только тот код, который используется. Для того чтобы свернуть или развернуть нужный блок, специально предусмотрены значки [-] и [+] в левой части редактора. Если нажать на значок [-], например, возле определения метода, код этого метода будет свернут, то есть, убран из видимости. Но, кроме этого, есть возможность применить эту операцию ко всему коду, а не только к текущему месту.

В меню по правой кнопке мыши есть два пункта Fold и UnFold. Это, соответственно, операции "свернуть" и "развернуть". Для каждой из них нужно указать место действия. Например, свернуть все методы в коде или все определения типов. Хочется заметить, что "свернутая" часть кода никуда не девается, а лишь уходит из видимой части редактора. Так что, если при компиляции или во время работы "Error Insight", ошибка окажется в свернутом коде, он прекрасным образом будет развернут автоматически в нужном месте. Так что никакой путаницы не возникнет.

Кроме этих возможностей, введены две директивы, которые по синтаксису аналогичны директивам компилятора, но оказывают влияние на поведение редактора, а не на генерируемый код. Это директивы $REGION и $ENDREGION. Они задают начало и конец сворачиваемого региона кода. Можно задать имя региона, в этом случае, когда регион свернут, вместо многоточия отображается имя региона (см. ).



Sync Edit


При выделении части кода в редакторе, на левой полосе, вслед за выделяемыми строками, передвигается маленькая иконка (на рисунке она отмечена красным). Это включение режима "Sync Edit". Если нажать на эту иконку, то выделенный текст подкрасится (при цветовых настройках по умолчанию) голубым цветом и будет выделен первый в блоке идентификатор.

Открыть скриншот в отдельном окне

Суть режима "Sync Edit" в том, что он позволяет показать в выделенном тексте все повторяющиеся идентификаторы, их может оказаться несколько групп. Самая первая считается текущей.

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

В отличие от полноценного рефакторинга, в режиме "Sync Edit" не производится никакого синтаксического анализа, а просто ищется повторяющееся имя. Его можно применять, как "быстрый рефакторинг" или использовать для поиска всех мест, где используется искомый идентификатор.

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

Повторное нажатие на иконку на левой полосе редактора кода, выключает "Sync Edit" и возвращает обычный режим редактирования.



Таблицы


Для таблиц определены следующие операции:

Просмотр данных

Это операция по умолчанию — двойной клик по имени таблицы в списке. Открывается отдельное окно с содержимым таблицы. Возможно редактировать все поля, даже identity и computed column. Диагностика производится в момент сохранения изменений (правая кнопка мыши | Update/Rollback), так что испортить таблицу затруднительно.

Изменение структуры

Визуальный аналог команды "Alter table".

В этом окне по правой кнопке мыши доступны команды Save changes/Show DDL/ Exeсute DDL

DDL (Data definition language) — текст SQL-скрипта, который отражает сделанные визуально изменения в структуре таблицы.

Удаление таблицы

Выполнение команды "Drop table"

Копирование таблицы

Копирование существующей таблицы в новую. Происходит, как создание новой таблицы с такой же структурой (имя новой таблицы запрашивается при выполнении paste) и заполнении ее данным из копируемой таблицы.



Unicode-идентификаторы


Появилась возможность использовать в именах типов и переменных символы Unicode.

Запись вида

type Работник = record Фамилия: string; Имя: string; Отчество: string; ДатаРождения: TdateTime; Должность: string; end;

выглядит понятнее, чем

Rabotnik = record Familija: string; Imya: string; Ochestvo: string; DataRogdenija: TdateTime; Dolgjnost: string; end;

Разумеется, такой код будет многим непривычен и вызовет недовольные нарекания в духе "настоящий программист должен писать имена переменных/типов/методов на английском языке", но давайте помнить, что программа пишется в первую очередь для человека, компилятору абсолютно все равно, какие имена будут объявлены в программе.

Пример компилирующегося и работающего кода:

type Целое = Integer; procedure TfMain.BtnCountClick(Sender: TObject); var Счетчик: Целое; begin for Счетчик:=0 to 5 do begin btnCount.Caption := IntToStr(Счетчик); Sleep(500); Application.ProcessMessages; end; end;



Unit-тестирование


"Мой опыт показывает, что создав хорошие тесты, можно значительно увеличить скорость программирования"
(с) Мартин Фаулер.

Delphi 2005 располагает встроенными средствами для организации тестирования работы отдельных модулей программы, основанными на известных open-source проектах DUnit и NUnit (.NET). Среда позволяет создать проект-оболочку для тестов и шаблоны тестирующих модулей. Рассмотрим возможности Delphi 2005 на примере тестирования простого класса, осуществляющего перевод чисел из двоичной формы в символьную по заданному основанию системы счисления и, наоборот, из символьной в двоичную.

Создадим класс, методы которого будут выполнять перевод, а основание системы счисления будет являться свойством класса.

Реализация метода ToString будет содержать ошибки, которые мы будем обнаруживать тестированием. Первая реализация выглядит так:

unit Convertor; interface type TNumericConvertor = class private FBase: Integer; public constructor Create (const ABase: Integer); property Base: Integer read FBase; function ToString (const Value: Integer): string; function ToNumber (const Value: string): Integer; end; implementation { TNumericConvertor } constructor TNumericConvertor.Create(const ABase: Integer); begin Assert ((ABase > 1) and (ABase <= 36), 'Illegal Base specfied'); FBase := ABase; end; function TNumericConvertor.ToNumber(const Value: string): Integer; var I, Digit: Integer; begin Result := 0; for I:=1 to Length(Value) do begin if Value[I] > '9' then Digit := Ord(Value[I]) - Ord('A') + 10 else Digit := Ord(Value[I]) - Ord('0'); Assert ((Digit >= 0) and (Digit < Fbase), 'Illegal character'); Result := Result * FBase + Digit; end; end; function TNumericConvertor.ToString(const Value: Integer): string; var Rem, Quot: Integer; begin Assert (Value >= 0, 'Only positive numbers can be converted'); Result := ''; Quot := Value; while Quot <> 0 do begin Rem := Quot mod FBase; if Rem >= 10 then Result := Result + Char(Rem + Ord('0')) else Result := Result + Char(Rem + Ord('A') - 10); Quot := Quot div Fbase; end; if Result = '' then Result := '0'; end; end.

Создадим проект-оболочку для тестов командой File|New|Other выбрав в категории Unit Tests элемент Test Project (см. рис. 1 и 1-1).

Показать скриншоты в отдельном окне: рисунок 1 и рисунок 1-1

После этого группа проектов принимает вид:

Добавим в эту оболочку первый тестирующий модуль командой File|New|Other выбрав в категории Unit Tests элемент Test Case.

Показать скриншоты в отдельном окне:

Добавление первого модуля, шаг 1 Добавление первого модуля, шаг 2

В результате этих действий в IDE открывается окно с кодом сгенерированного класса для тестирования методов выбранного класса.

unit TestConvertor; { Delphi DUnit Test Case ---------------------- This unit contains a skeleton test case class generated by the Test Case Wizard. Modify the generated code to correctly setup and call the methods from the unit being tested. } interface uses TestFramework, Convertor; type // Test methods for class TNumericConvertor TestTNumericConvertor = class(TTestCase) strict private FNumericConvertor: TNumericConvertor; public procedure SetUp; override; procedure TearDown; override; published procedure TestToString; procedure TestToNumber; end; implementation procedure TestTNumericConvertor.SetUp; begin FNumericConvertor := TNumericConvertor.Create; end; procedure TestTNumericConvertor.TearDown; begin FNumericConvertor.Free; FNumericConvertor := nil; end; procedure TestTNumericConvertor.TestToString; var ReturnValue: string; Value: Integer; begin // TODO: Setup method call parameters ReturnValue := FNumericConvertor.ToString(Value); // TODO: Validate method results end; procedure TestTNumericConvertor.TestToNumber; var ReturnValue: Integer; Value: string; begin // TODO: Setup method call parameters ReturnValue := FNumericConvertor.ToNumber(Value); // TODO: Validate method results end; initialization // Register any test cases with the test runner RegisterTest(TestTNumericConvertor.Suite); end.

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

В методе Setup пишем код для вызова корректного конструктора

procedure TestTNumericConvertor.SetUp; begin FNumericConvertor := TNumericConvertor.Create (10); end;

Метод TestToString принимает вид:

procedure TestTNumericConvertor.TestToString; var ReturnValue: string; Value: Integer; begin Value := 10; ReturnValue := FNumericConvertor.ToString(Value); Assert (ReturnValue = '10', 'Expect ''10'', receive '''+ReturnValue+''''); end;

И последний метод - TestToNumber

procedure TestTNumericConvertor.TestToNumber; var ReturnValue: Integer; Value: string; begin Value := '10'; ReturnValue := FNumericConvertor.ToNumber(Value); Assert (Returnvalue = 10, 'Expect 10, receive '+IntToStr(ReturnValue)); end;

Компилируем и запускаем тестовый проект, его окно выглядит .

После запуска тестов видно, что один из методов исходного класса работает некорректно, так как полученный результат не соответствует ожидаемому (Ожидается '10' получен 'AB')

Показать скриншот в отдельном окне

Анализируя исходный код метода, видно, что при переводе очередного знака числа, условия then и else необходимо поменять местами:

if Rem >= 10 then Result := Result + Char(Rem + Ord('A') - 10) else Result := Result + Char(Rem + Ord('0'));

Перекомпилировав проект после исправления, снова запускаем тесты.

Видно, что ошибка исправлена, но метод все работает не так, как ожидается (Ожидается '10', получено '01').

Показать скриншот в отдельном окне

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

if Rem >= 10 then Result := Char(Rem + Ord('A') - 10) + Result else Result := Char(Rem + Ord('0')) + Result;

Снова компилируем тестовый проект, после запуска убеждаемся, что исправленный метод теперь работает как ожидалось, при заданных в тесте условиях.

Показать скриншот в отдельном окне

Это не окончательный показатель гарантии правильной работы методов класса, для полной проверки необходим еще ряд тестов, тем не менее, две ошибки выявлены тестами за короткое время.

Для проверки перевода чисел в другой системе счисления можно создать еще один Test Case, например, тестирующий перевод из двоичного вида в символьный и обратно в двоичной системе счисления.

unit TestConvertor1; { Delphi DUnit Test Case ---------------------- This unit contains a skeleton test case class generated by the Test Case Wizard Modify the generated code to correctly setup and call the methods from the unit being tested. } interface uses TestFramework, Convertor; type // Test methods for class TNumericConvertor TestTNumericConvertor1 = class(TTestCase) strict private FNumericConvertor: TNumericConvertor; public procedure SetUp; override; procedure TearDown; override; published procedure TestToString; procedure TestToNumber; end; implementation uses SysUtils; procedure TestTNumericConvertor1.SetUp; begin FNumericConvertor := TNumericConvertor.Create(2); end; procedure TestTNumericConvertor1.TearDown; begin FNumericConvertor.Free; FNumericConvertor := nil; end; procedure TestTNumericConvertor1.TestToString; var ReturnValue: string; Value: Integer; begin Value := 11; ReturnValue := FNumericConvertor.ToString(Value); Assert(ReturnValue = '1011', 'Expect ''1011'', receive '''+ReturnValue+''''); end; procedure TestTNumericConvertor1.TestToNumber; var ReturnValue: Integer; Value: string; begin Value := '1011'; ReturnValue := FNumericConvertor.ToNumber(Value); Assert(ReturnValue = 11, 'Expect 11, receive '+IntToStr(ReturnValue)); end; initialization // Register any test cases with the test runner RegisterTest(TestTNumericConvertor1.Suite); end.

После компиляции и запуска тестового проекта, видно, что новые тесты добавились к старым, так что любые исправления исходного кода можно протестировать как всеми созданными тестами, так и выбрав конкретные тесты.

Показать скриншот в отдельном окне

В заключение хочется добавить, что использовать unit-тесты можно было и раньше. Например, полтора года назад, одним из авторов обзора была разработана среда подобного рода для того, чтобы полностью осознать, что же такое unit-тесты. Среда включала в себя главную программу и эксперт для генерации тестирующих модулей. Разумеется, не с таким красивым и удобным интерфейсом, гораздо больше кода приходилось писать вручную, но главный итог такой разработки и использования — осознание необходимости тестирования, в особенности, использования unit-тестов как значительного подспорья для разработки программ.

Спасибо фирме Borland, что такие нужны и удобные средства уже встроены в их новый продукт — Delphi 2005.



Unsafe Code


Для компилятора Delphi for .NET добавлена возможность включения небезопасного кода в приложения .NET. Для этого введена локальная директива компилятора {$UNSAFECODE}, которая может принимать значения ON и OFF а также добавлено ключевое слово unsafe, которое применяется к процедурам и функциям. Приложения, использующие небезопасный код не проходит проверку при помощи утилиты PEVerify. Подробнее о небезопасном коде смотрите в документации .NET SDK.

procedure unsafeProc; unsafe; begin end;



Вложенные типы данных и константы


Эта возможность также является новшеством только для компилятора Delphi for Win32. Теперь, как и в случае с Delphi for .NET, можно объявлять типы данных и константы внутри других классов.

type TOuterClass = class public const x = 12; i: integer = 1; type TInnerClass = class public myInnerField: Integer; procedure innerProc; end; procedure outerProc; end;

Обращение ко вложенным типам и константам производится через имя типа, в который они вложены, например, TOuterClass.TInnerClass или TOuterClass.x. Для вложенных типов и констант действуют те же модификаторы видимости, что и для остальных членов классов.

Существует одна интересная особенность. Хотя компилятор для Win32 не поддерживает полей класса, их в какой-то мере можно заменить вложенными типизированными константами при условии, что включена опция компилятора $J (она же $WRITEABLECONST).



Встроенный Data Explorer


В IDE интегрирован Data Explorer, который содержит как средства просмотра базы данных, так и ряд инструментов для редактирования. Окно Data Explorer можно найти на одной из закладок окна Project Manager справа от редактора кода (при умолчанных настройках среды) или в меню View | Data Explorer

Выбираете провайдера для вашей БД, настраиваете коннекцию к базе и получаете список ее объектов:



XML Documentation


Для компилятора Delphi for .NET эта возможность существует с версии Delphi 8. Теперь эта возможность доступна и в компиляторе для Win32. Компилятор умеет различать в исходном тексте специальным образом оформленные комментарии и генерировать на их основе XML файлы. Формат комментариев во многом похож на XML. Каждый комментарий, который будет анализироваться на наличие тегов XML документации предшествовует документируемому объекту и должен начинаться с комбинации из трёх символов "/". Существует набор тегов, которые рекомендуется применять при оформлении комментариев. Он описан в справке .NET SDK. К сожалению для тех, кто не любит писать всякие теги руками, IDE никак не облегчает оформление таких комментариев.

Примитивный пример оформления документации:

type ///<summary> Test comment ///</summary> TForm3 = class(TForm) private { Private declarations } ///<summary> Test comment 1 ///</summary> procedure Test; public { Public declarations } end;

Примерный вид XML документации, генерируемой компилятором:

<?xml version="1.0" encoding="utf-8"?> <namespace name="Unit3"> <class name="TForm3"> <devnotes> <summary> Test comment </summary> </devnotes> <ancestor name="TForm" namespace="Forms"> <methodref name="ArrangeIcons" visibility="public" /> <methodref name="Cascade" visibility="public" /> здесь перечисляются предки и все их члены </ancestor> <members> <procedure name="Test" visibility="private"> <devnotes> <summary> Test comment 1 </summary> </devnotes> </procedure> </members> </class> <variable name="Form3" type="TForm3" /> </namespace>