Эксперты в Delphi — что это такое?
Если не хватает возможностей среды или какие-то операции кажутся слишком громоздкими, то эксперты — именно то, что нужно. С помощью экспертов вы словно проникаете внутрь среды Delphi и без труда дополняете ее. Естественно, такое проникновение должно быть осторожным и аккуратным, потому как неправильное обращение с объектами и интерфейсами может вызвать сбои в работе среды или даже ее разрушение. Эксперты могут существовать в виде библиотек DLL либо компилированных модулей DCU. Выбор “формы жизни” будущего эксперта остается за вами, но имейте в виду, что расширение файла эксперта определяет способ его регистрации. О способах регистрации чуть далее — сперва давайте рассмотрим стили экспертов Delphi. Их всего четыре, и все они приведены в таблице.
Эксперты в Delphi, или Программист, упростите себе жизнь
Олег Гопанюк, ведущий программист департамента "KM-Solution" корпорации "Квазар-Микро",
Имеющее множество достоинств и довольно популярное средство разработки Delphi позволяет расширять функциональные возможности среды разработчика. Речь идет не о косметических изменениях в интерфейсе и не о добавлении компонентов или их наборов, а о придании рабочему окружению программиста новых полезных возможностей, не предусмотренных его создателями. Для решения подобной задачи в Delphi можно воспользоваться так называемыми экспертами. Вспомните, как удобно, ответив на несколько вопросов, создать готовую форму для вывода содержимого таблиц. Или, щелкнув мышью на нужном элементе в списке New Items, получить “костяк” вашего будущего проекта (рис. 1).
Рис. 1. Многие возможности Delphi реализуются с помощью экспертов
Что это — стандартные возможности рабочей среды? Да, но применить их можно лишь с помощью эксперта. О том, как это сделать, и пойдет речь далее.
Некоторые полезные эксперты
Знаете ли вы, что в Internet есть предостаточно мест, где можно найти эксперты для Delphi. Одно из таких мест — польский сервер “Delphi Super Page” (). Там вы найдете множество различных экспертов и полезных компонентов. Давайте рассмотрим самый интересный, по мнению автора, набор экспертов, предоставляющий возможность ускорить разработку приложений на Delphi. Его можно загрузить по адресу: .
Рассмотрим вкратце эти маленькие “добавки”. Набор содержит эксперт — редактор префиксов для имен компонентов. После того, как он будет установлен в инспекторе объектов, напротив свойства Name появится кнопка с многоточием. Это говорит о том, что можно воспользоваться редактором для изменения свойства Name. С его помощью можно указывать префикс для данного класса компонента. Строго говоря, использование префиксов в названиях компонентов — это правило хорошего тона. В меню Tools теперь появляется новое подменю Prefix list editor, с помощью которого можно изменять и добавлять такие префиксы.
Как известно, некоторые компоненты являются контейнерами для других (например, TPanel, TGroupBox, TScrollBox и т. п.). Установленный набор позволит управлять выравниванием дочерних компонентов. Для этого достаточно щелкнуть правой кнопкой мыши и выбрать в контекстном меню пункт Align controls. В Delphi есть мастер создания элементов управления, работающих с данными.
Однако в рассматриваемом наборе имеется эксперт, благодаря которому можно создавать компоненты для работы с данными более совершенным способом. С помощью эксперта, вызываемого командой Tools р Shortcut list editor, можно определить свой набор клавиатурных эквивалентов для главного меню Delphi. Кроме всего прочего, после установки набора вы обнаружите, что палитра компонентов Delphi стала многострочной (). Так вы получите возможность просматривать больше закладок, чем ранее.
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 |
Внимание! Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав. |
Супер предложение: от авторитетного интернет-магазина.
|
Open Tools API
Open Tools API — это набор интерфейсов; они предоставляют доступ к среде Delphi и позволяют управлять файлами и проектами. Основной объект Open Tools API — ToolServices — это глобальная переменная. При запуске Delphi создается экземпляр класса TIToolServices, и переменной ToolServices присваивается ссылка на него. Эксперты могут использовать ToolServices для доступа к функциям среды разработки.
Любой сервис, предоставляемый Open Tools API, прямо или косвенно вызывается через ToolServices. В приведено краткое описание Open Tools API.
Переопределение методов — задача довольно простая; она предполагает написание всего нескольких строк кода. Например, реализация метода GetStyle вряд ли отнимет у вас много времени:
function MyExpert.GetStyle:
TexpertStyle
begin
Result := [esStandard];
end;
Реализация класса TIExpert
Для создания нового эксперта необходимо наследовать новый класс от класса TIExpert, переопределив при этом часть его методов (таблица 2)
Возможность переопределени методов экспертов тех или иных стилей
Все девять методов () предоставляют информацию об эксперте и организуют его взаимодействие со средой. TIExpert — это абстрактный виртуальный класс с заданными, но не реализованными функциональными возможностями. От этого класса будут порождены другие, имеющие необходимые возможности.
Определение класса TIExpert приведено далее.
TIExpert = class(TInterface)
public
{ Методы пользовательского
интерфейса с экспертом }
function GetName: string;
virtual; stdcall; abstract;
function GetAuthor: string;
virtual; stdcall; abstract;
function GetComment: string;
virtual; stdcall; abstract;
function GetPage: string;
virtual; stdcall; abstract;
function GetGlyph: HICON;
virtual; stdcall; abstract;
function GetStyle:
TExpertStyle; virtual; stdcall;
abstract;
function GetState:
TExpertState; virtual; stdcall;
abstract;
function GetIDString: string;
virtual; stdcall; abstract;
function GetMenuText: string;
virtual; stdcall; abstract;
{ Запуск эксперта }
procedure Execute; virtual;
stdcall; abstract;
end;
Регистрация экспертов
Зарегистрировать эксперт можно одним из двух способов. Первый способ сводится к определению эксперта как компонента путем вызова процедуры RegisterLibraryExpert из процедуры Register. Второй способ заключается в создании DLL-библиотеки эксперта. Преимущество первого способа в том, что не приходитс закрывать среду Delphi при внесении изменений в эксперт — достаточно его перекомпилировать. Сперва рассмотрим регистрацию эксперта как компонента. Необходимо добавить в модуль эксперта процедуру Register:
Procedure Register;
Implementation {$R*.DFM}
Procedure Register;
Begin
RegisterLibraryExpert
(TPowerExpert. Create);
// TpowerExpert — это класс регистрируемого эксперта
End;
Для регистрации эксперта как DLLбиблиотеки следует выполнить две операции: реализовать новый проект DLL и изменить содержимое системного реестра Windows. Итак, создаем DLL. Выполните команду File р New, а затем укажите Delphi, что необходимо создать DLL. В результате появится новое окно модуля с неким набором исходного кода. После этого следует экспортировать функцию InitExpert. Обратите внимание, что эта функция экспортируется с помощью специальной константы ExpertEntryPoint, которую Delphi определяет для всех экспертов, создаваемых в виде DLL. Основное назначение функции InitExpert — возврат ссылки на объект ToolServices для дальнейшего использования и вызова процедуры RegisterProc, которая, собственно, и регистрирует эксперт. Ниже приведена реализация этой функции:
Function InitExpert(
ToolServices:ToolServices;
RegisterProc:TexpertRegisterProc;
var
Terminate:TExpertTerminateProc):
Boolean; export; stdcall;
implementation
procedure TerminateExpert;
begin
// завершение работы эксперта
end;
function InitExpert(
ToolServices:ToolServices;
RegisterProc:TExpertRegisterProc;
var
Terminate:TExpertTerminateProc):
Boolean; export; stdcall;
begin
Result:=False;
end;
// проверка, является ли запущенное приложение единственным
if (ToolServices=nil) or Assigned(ExptIntf.ToolServices)
then Exit;
ExptIntf.ToolServices:=ToolServices;
//сохраняем указатель на ToolServices
Application.Handle:=
ToolServices.GetParentHandle;
//сохраняем указатель на
ToolServices для родительского
окна
Terminate:=TerminateExpert;
//устанавливаем процедуру завершения
RegisterProc(TGenericExpert.Create);
//регистрация эксперта
Result:=True;
end;
Когда DLL с экспертом будет готова, от вас потребуется лишь изменить системный реестр так, чтобы Delphi “знала” расположение библиотеки с экспертом и смогла ее загрузить. Для этого с помощью редактора реестра (regedit.exe) добавьте в реестр такую запись:
HKEY_CURRENT_USER\Software\Borland\ Delphi\4.0\Experts
MyExpert=C:\MyExpertts\MyExpert.DLL
Для того чтобы среда зарегистрировала DLL, Delphi необходимо перезапустить. Вариант реализации эксперта в виде DLL кажетс автору менее удобным: перезагрузка среды отнимает больше времени по сравнению с перекомпиляцией библиотеки компонентов, что особенно ощутимо при отладке эксперта. Еще одна проблема — неполна совместимость экспертов в виде DLL, которые были созданы и скомпилированы для других версий Delphi. Автор надеется, что эта стать поможет профессионалам поближе познакомиться с экспертами Delphi. Возможно, ее публикация подтолкнет многих программистов к изучению темы.
Стили экспертов
Стиль |
Способ вызова |
Стандартный |
Добавить в подменю Help пункт меню |
Надстройки |
То же в определенное экспертом подменю пункт меню |
Формы |
- // - во вкладку Forms диалогового окна New
Items пиктограмму эксперта |
Проекта |
- // - во вкладку Projects диалогового окна New
Items то же |
Главное отличие между стилями заключается в способе вызова эксперта пользователем в среде Delphi. Как видите, можно определить самый удобный из них. Реализация экспертов предполагает использование интерфейса Open Tools API — набора классов, позволяющего обращаться ко множеству функций среды Delphi. В экспертах Open Tools API может использоваться для: получения информации о проекте; получения информации о модуле или форме; управления модулями проекта (дл открытия, закрытия, добавления, создания); управления ресурсами проекта; модификации меню Delphi; регистрации изменений в проекте; регистрации изменений в модуле.
Следует заметить, что интерфейс Open Tools API доступен только из программ, запущенных как часть интегрированной среды Delphi. В следующем разделе мы рассмотрим несколько полезных экспертов.
Часть I
В данной статье излагается материал о проблемах связанных с созданием анимированных 3D пресонажей в приложениях использующих OpenGl. Статья расчитана в основном на продвинутого читателя. Для наилучшего восприятия желательно иметь опыт работы со средой программирования Delphi, а также весьма не помешает знать библиотеку OpenGL, графический пакет 3D Studio Max и его расширение Character Studio. Все это требуется потому, что данная статья не является справочником ни по одному из указанных инструментов и содержит описание только тех действий, которые необходимо выполнить для экспорта персонажей из 3D Studio Max и вывода их на экран средствами Delphi и OpenGL.
Что такое нормали?
Нормалью называется перпендикуляр к чему-либо. В нашем случае это перпендикуляр к грани. Хотелось бы, но, к сожалению, без нормалей никак не обойтись. Дело в том, что по нормалям расчитывается освещение объекта. Так, например, если нормаль грани направлена на источник света, то грань будет освещена максимально. Чем больше нормаль отвернется от источника света, тем менее грань будет освещена. В случае с OpenGL, если нормаль отвернется от экрана более чем на 90 градусов, мы вообще не увидим грань, она не будет отрисовываться. Если бы мы не использовали нормали, то наш объект был бы закрашен одним цветом, то есть мы бы увидели только силует объекта. Трехмерный эффект достигается окрашиванием граней объекта в разные по яркости цвета, или наложением теней, кому как больше нравится это называть. Кроме того, степень освещенности зависит также от длины вектора нормали, но, как правило, длина вектора нормали должна находится в пределах (0; 1).
Теперь я думаю, стало ясно, что такое нормали и зачем они нужны.
Формат файла GMS
Файл GMS это текстовый файл открытого формата, что означает, что даже человек не знакомый с его описанием может создать приложение, считывающее из него информацию. Тем не менее, приведу на всякий случай описание этого файла. New object // Указывает на начало нового объекта, // следующая строка указывает тип объекта
TriMesh() // Объект - сетка
numverts numfaces // Указывает, что следующая строка // содержит количество вершин // и граней для данного объекта
Mesh vertices:
// Здесь располагается блок вершин объекта // в виде координат X Y Z
end vertices
Mesh faces:
// Здесь располагается блок граней объекта в виде // индексов 1 2 3, где каждый индекс - индекс // в массиве вершин, указывает на вершину грани
end faces
Faset normals:
// Здесь располагается блок фасетных нормалей // в виде координат X Y Z. // Их количество равно количеству граней
end faset normals
Smooth normals:
// Здесь располагается блок сглаживающих нормалей // в виде координат X Y Z. // Их количество равно количеству вершин.
end smooth normals
end mesh // Конец описания объекта Tri Mesh
end of file // Конец файла
Примерно так выглядит файл, когда мы экспортируем сетчатый объект. Если объект не сетчатый, то файл будет выглядеть следующим образом: New object // Указывает на начало нового объекта, // следующая строка указывает тип объекта
<Тип объекта>, например: Box
// Здесь идут параметры, зависящие от типа объекта // (Поверхности Безье и NURBS - // поверхности не поддерживаются)
end <Тип объекта> // Конец описания объекта
end of file // Конец файла
Рекомендуемая литература
"Эффективная работа с 3D Studio MAX 2", Майкл Тодд Петерсон при участии Ларри Минтона. "Анимация персонажей в 3D Studio MAX", Стефани Рис. "OpenGL графика в проектах Delphi", Михаил Краснов.
Скачать :
Полный архив : ( 561 K) файлы проектов + GMS файлы + утилита MEGA.ms
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 |
Внимание! Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав. |
Рекомендуем купить от авторитетной компании.
|
Соглашения
Автор дает полное право всем желающим на копирование, распространение и модификацию файлов примеров программ. Авторские права на данную статью принадлежат Ивану Дышленко. Право на копирование и изменение любой части текста данной статьи принадлежит только автору, данную статью разрешается копировать и распространять только полностью, с файлами и примерами программ. Не разрешается модифицировать и распространять модифицированные варианты утилиты MEGA.ms, поскольку утилита будет наращиваться и автору хотелось бы избавить пользователей от проблем с вопросами совместимости.
Создание анимированного персонажа и вывод на экран
Специально для тех, кто не владеет навыками работы с 3D Studio Max и Character Studio, я создал модель бегающего человечка. Она находится в папке MAX, и файл называется BodyRun.max. Если у Вас вообще нет пакета 3D Studio Max, то файл GMS с сетками этого человечка находится в папке GMS и называется ManRun.gms.
Итак, запустите среду 3D Studio Max и создайте анимированного персонажа или загрузите его из файла BodyRun.max. Запустите утилиту MEGA, как это делалось в разделе Знакомство с утилитой MEGA V1.0. Установите значение поля From =0, значение поля To установите в кадр, на котором заканчивается анимация, в случае с файлом BodyRun.max это значение нужно установить в 11. Значение поля Step установите в еденицу. Выделите сетку персонажа.
Внимание: убедитесь, что Вы выделили именно сетку персонажа и только ее. Пометьте флажок Selected Only. Для анимации сетки используется скелет. Он создается и подгоняется под размеры и форму тела, затем вершины сетки связываются с костями скелета. При анимации изменяются параметры положения частей скелета, а сетка лишь следует за ними. Поэтому, всегда, когда используется этот подход, в сцене помимо сетки присутствует скелет. Вот почему необходимо выделить только сетку и пометить флажок Selected Only.
После того, как Вы выполнили все операции укзанные выше, экспортируйте объект в файл GMS. В процессе экспорта Вы должны увидеть, как последовательно перемещается ползунок расположенный внизу экрана, отсчитывая кадры анимации, и как меняются кадры в проекционных окнах 3D Studio Max. Процесс экспорта завершится, когда ползунок достигнет конечного значения.
Готовый проект лежит в папке Ch02. Откомпилируйте его и запустите на выполнение. На экране вы должны увидеть примерно то, что изображено на рисунке. Нажатием кнопки "Анимировать" можно запускать или останавливать анимацию. Если Ваш компьютер оснащен 3D ускорителем, то лучше развернуть окно на весь экран - так медленнее. Теперь разберем исходный код программы. Он дополнился новым объектом TGLMultyMesh, который создан для загрузки и последовательной отрисовки нескольких сетчатых объектов. TGLMultyMesh = class Meshes : TList; CurrentFrame : Integer; Action : Boolean; fExtent : GLFloat; Extent : Boolean; public procedure LoadFromFile( const FileName : String ); procedure Draw; constructor Create; destructor Destroy; override; published
end;
Список Meshes хранит все сетки загруженные из файла. Переменная Action указывает выполняется анимация или нет, а CurrentFrame содержит номер текущего кадра анимации. procedure TGLMultyMesh.LoadFromFile; var
f : TextFile; S : String; procedure ReadNextMesh; var
i : Integer; Vertex : TGLVertex; Face : TGLFace; MaxVertex : GLFloat; NextMesh : TGLMesh;
begin
NextMesh := TGLMesh.Create;
repeat ReadLn(f, S); until (S = 'numverts numfaces') or eof(f);
// Читаем количество вершин и граней
Readln(f,NextMesh.VertexCount,NextMesh.FacesCount);
// Выделяем память для хранения сетки
GetMem(NextMesh.Vertices, NextMesh.VertexCount*SizeOf(TGLVertex)); GetMem(NextMesh.Faces, NextMesh.FacesCount*SizeOf(TGLFace)); GetMem(NextMesh.FasetNormals, NextMesh.FacesCount*SizeOf(TGLVector));
ReadLn(f,S); // Пропускаем строку Mesh vertices:
// Считываем вершины
for i := 0 to NextMesh.VertexCount - 1 do begin
Readln(f,Vertex.x,Vertex.y,Vertex.z); NextMesh.Vertices[i] := Vertex;
end;
ReadLn(f,S); // Пропускаем строку end vertices ReadLn(f,S); // Пропускаем строку Mesh faces:
// Считываем грани
for i := 0 to NextMesh.FacesCount - 1 do begin
Readln(f,Face[0],Face[1],Face[2]); Face[0] := Face[0] - 1; Face[1] := Face[1] - 1; Face[2] := Face[2] - 1; NextMesh.Faces[i] := Face; end; // Рассчитываем масштаб
MaxVertex := 0;
for i := 0 to NextMesh.VertexCount - 1 do begin
MaxVertex := Max(MaxVertex,NextMesh.Vertices[i].x); MaxVertex := Max(MaxVertex,NextMesh.Vertices[i].y); MaxVertex := Max(MaxVertex,NextMesh.Vertices[i].z); end;
NextMesh.fExtent := 1/MaxVertex;
NextMesh.CalcNormals;
Meshes.Add(NextMesh);
end;
begin
Meshes := TList.Create;
AssignFile(f,FileName);
Reset(f);
While not Eof(f) do begin
Readln(f,S);
if S = 'New object' then ReadNextMesh;
end;
CloseFile(f);
end;
Код загрузки объекта TGLMultyMesh практически идентичен коду загрузки объекта TGLMesh. Небольшое отличие состоит в том, что объект TGLMultyMesh предполагает, что файл содержит несколько сеток. Поэтому при загрузке проиходит поиск строки "New Object", создается объект TGLMesh, который помещается в список Meshes и в него считывается информация из файла. Затем весь цикл повторяется до тех пор, пока не кончится файл. Процедуры создания, уничтожения и отрисовки объекта тоже почти не изменились: procedure TGLMultyMesh.Draw; begin
if Extent then begin
fExtent := TGLMesh(Meshes.Items[CurrentFrame]) .fExtent; glScalef(fExtent,fExtent,fExtent);
end;
// Рисование текущего кадра TGLMesh(Meshes.Items[CurrentFrame]).Draw; // Если включена анимация // увеличить значение текущего кадра
if Action then begin
inc(CurrentFrame); if CurrentFrame > (Meshes.Count - 1) then CurrentFrame := 0;
end;
end;
constructor TGLMultyMesh.Create; begin
Action := False;
CurrentFrame := 0;
end;
destructor TGLMultyMesh.Destroy; Var i : Integer; begin
for i := 0 to Meshes.Count - 1 do begin
TGLMesh(Meshes.Items[i]).Destroy;
end;
Meshes.Free;
end;
Немного изменился и вызов функции загрузки в модуле frmMain.pas. procedure TfrmGL.N1Click(Sender: TObject); begin
if OpenDialog.Execute then begin
MyMesh.Destroy;
Mymesh := TGLMultyMesh.Create;
MyMesh.LoadFromFile( OpenDialog.FileName );
MyMesh.Extent := true;
// Проверяем сколько сеток загружено // и возможна ли анимация
if MyMesh.Meshes.Count <= 1 then N2.Enabled := False else N2.Enabled := True;
end;
end;
// Включение анимации procedure TfrmGL.N2Click(Sender: TObject); begin
MyMesh.Action := not MyMesh.Action; N2.Checked := not N2.Checked;
end;
Здесь все должно быть предельно ясно, не будем акцентировать на этом внимание, и так статья длиннее получается, чем я расчитывал.
Да, конечно, человечек убогий. Мало того, что он кривой, так еще и прихрамывает. Что делать, чтобы создавать красивых человечков с минимальным количеством граней нужно быть профессионалом 3D моделирования. Все же, мы еще попытаемся его улучшить.
Вероятно, Вы заметили, огрехи воспроизведения объектов на экране, выражающиеся в каких - то непонятных черных треугольниках в тех местах, где их не должно быть. Сам я понятия не имею, откуда они взялись. Если Вас не удовлетворяет такой вид объектов, значит, настала пора поговорить о нормалях.
я здорово помучался, решая вопрос
В свое время я здорово помучался, решая вопрос - каким же образом создатели игр ухитряются делать трехмерные персонажи двигающиеся в реальном времени. Я предположил, что части тела у персонажей отделены от основного тела, что позволяет независимо поворачивать и перемещать их. Знаете, в Direct3D даже есть понятие фрейма, фрейм - это основное тело, к нему прикрепляются другие тела. Когда фрейм движется, прикрепленные к нему объекты движутся вместе с ним, кроме того, прикрепленные объекты могут двигаться и самостоятельно не влияя на движение фрейма. Все это замечательно подходит для создания механических объектов и персонажей, но совершенно не годится для создания объектов живого мира. Для таких объектов характерна плавность линий и отсутствие изломов на местах стыков частей объекта. Создатели компьютерных игр замечательно решили эту проблему.
Как создается двумерная анимация? Рисуется несколько кадров движения, затем поледовательно выводятся на экран и таким образом создается иллюзия движения. То же самое происходит в современных трехмерных компьютерных играх. Создается несколько 3D моделей (сеток), характеризующих фазы движения персонажа в различные моменты времени, затем поледовательно выводятся на экран, создавая иллюзию движения. Возможно, это приводит к повышенному расходу оперативной памяти, поскольку все сетки желательно хранить в памяти, но зато значительно упрощается процесс программирования и, скорее всего, увеличивается скорость работы приложения.
Следующая проблема возникла при попытке экспорта объектов из 3D Studio Max в какой-либо открытый формат, например DXF. Нет ничего сложного в создании трехмерного персонажа с последующей его анимаций, если пользоваться 3D Studio и Character Studio, вся проблема состоит в том, как экспортировать объект чтобы потом файл с сетками объекта можно было использовать в своем приложении. Для этого требуется покадровый экспорт анимированного персонажа, то есть в итоге должен получится файл, содержащий несколько сеток объекта изображающих фазы движений объекта в различные моменты времени, или несколько файлов содержащих одну сетку соответствующую определенному кадру движения. Однако, несмотря на обилие поддерживаемых форматов файлов, 3D Studio Max не обладает возможностью покадрового экспорта трехмерных объектов. Так, напрмер, файл формата 3DS может хранить информацию о положении объекта, его повороте и масштабе, но не в состоянии сохранять деформации сетки в различных кадрах анимации, а именно это нам и нужно. Про файлы формата DXF и ASC даже говорить в данном случае смешно. Я объясню, почему нам нужно сохранять именно деформацию сетки. Дело в том, что наш объект должен состоять из единой, цельной сетки, а не из нескольких объектов, чтобы не было стыков на местах соединений конечностей с телом. Создать анимацию, так чтобы персонаж мог двигать своими конечностями, в этом случае, можно только деформируя сетку, а именно перемещая одни вершины сетки относительно других. Так, например, чтобы персонаж поднял руку нужно переместить вершины руки вверх относительно вершин тела. Теперь, я надеюсь, все понятно? Итак, оказалось, что 3DStudio не в состоянии сохранить подобную анимацию. Однако, не все так печально. Например, есть такой дополнительный модуль для 3DStudio, называется Bones Pro Max, а у него есть инструмент SnapShot, который позволяет делать снимки различных кадров движения объекта. В результате его работы у Вас на рабочем поле 3D Studio Max появляется целое стадо одинаковых трехмерных объектов в различных позах. Правду сказать, я его не нашел, да и выпущен он был уже давно еще под первую версию 3D Studio Max. Поэтому я решил идти другим путем и окунулся во внутренний язык 3D Studi Max - Max Script. Результатом моей деятельности стала простенькая утилита Meshes Export for Games and Animation (MEGA), которая позволяет делать все, о чем я сказал выше и некоторые другие полезные вещи.
Загрузка файла формата GMS в Delphi
Пример загрузки файла GMS находится в папке Ch01. В проекте присутствует два модуля: frmMain.pas и Mesh.pas. Откомпилировав и запустив проект на выполнение вы должны увидеть вращающийся Тор (по-нашему: "Баранка"). Несмотря на то, что объект можно считать стандартным, он был в 3D Studio преобразован в сетку, поэтому в данном случае это именно сетчатый объект. Нажав пункт меню "загрузить", вы можете посмотреть любой объект из папки GMS или загрузить свою сферу, которую сделали сами, если правильно руководствовались моими инструкциями в разделе: Знакомство с утилитой MEGA V1.0. Теперь рассмотрим данный пример подробно. Почти весь код модуля frmMain.pas написан не мной. Он взят из книги "OpenGL графика в проектах Delphi" Михаила Краснова. Этот модуль выполняет инициализацию приложения и циклическую функцию отрисовки окна, поэтому подробно мы его рассматривать не будем. Если код покажется Вам непонятным, значит Вы недостаточно знакомы с OpenGL, в этом случае Вам надлежит обратится к первоисточнику (в смысле - к книге). Код модуля Mesh.pas выполняет загрузку данных из файла и отображение объектов в окне. Рассмотрим его подробнее: Type // Объявление типов данных
PGLVertex = ^TGLVertex; // Указатель на вершину
TGLVertex = record
x,y,z : GLFloat; // Вершина, как три // значения с плавающей точкой
end;
PGLVector = ^TGLVector; // Указатель на вектор
// Вектор, как массив из трех элементов // с плавающей точкой TGLVector = array[0..2] of GLFloat;
PGLFace = ^TGLFace; // Указатель на грань
// Грань, как массив из трех целочисленных значений TGLFace = array[0..2] of GLInt;
// Указатель на массив вершин PGLVertexArray = ^TGLVertexArray;
// Массив вершин TGLVertexArray = array[Word] of TGLVertex;
// Указатель на массив граней PGLFacesArray = ^TGLFacesArray;
// Массив граней TGLFacesArray = array[word] of TGLFace;
Здесь требуется небольшое пояснение. Как вы заметили, грань объявлена, как массив из трех целочисленных чисел. Дело в том, что граней почти всегда больше чем вершин. Поэтому все вершины запоминаются в отдельном массиве, а грань - это три индекса в этом массиве, указывающие на вершины принадлежащие грани. Одна вершина может принадлежать нескольким граням.
Теперь рассмотрим описание объекта сетка: TGLMesh = class // Массив вершин объекта - сетка Vertices : PGLVertexArray; // Массив граней Faces : PGLFacesArray; // Массив фасетных нормалей FasetNormals : PGLVertexArray; // Количество вершин VertexCount : Integer; // Количество граней FacesCount : Integer; // Коэффициент масштабирования fExtent : GLFloat; // Флаг масштабирования Extent : GLBoolean;
public
// Загрузка procedure LoadFromFile( const FileName : String );
procedure CalcNormals; // Расчет нормалей
procedure Draw; // Отрисовка
// Уничтожение с очисткой массивов destructor Destroy; override; end;
Здесь пояснений практически не требуется. Можно лишь отметить, что Extent служит для того, чтобы объект загнать в размеры в пределах (-1, 1), я сделал это для того, чтобы объект любого размера не мог вылезти за пределы окна. Вообще говоря, в 3D Studio Max не сложно масштабировать объект так, чтобы координаты вершин попали в интервал (-1, 1), но на этапе создания модели думать об этом совсем не хочется. procedure TGLMesh.LoadFromFile; // Загрузка файла var f : TextFile; S : String; i : Integer; Vertex : TGLVertex; Face : TGLFace; MaxVertex : GLFloat; begin
AssignFile(f,FileName); Reset(f); // Пропускаем строки, пока не попадется // 'numverts numfaces' repeat ReadLn(f, S); until (S = 'numverts numfaces') or eof(f); // Читаем количество вершин и граней
Readln(f,VertexCount,FacesCount); // Выделяем память для хранения сетки
GetMem(Vertices,VertexCount*SizeOf(TGLVertex)); GetMem(Faces,FacesCount*SizeOf(TGLFace)); GetMem(FasetNormals,FacesCount*SizeOf(TGLVector));
ReadLn(f, S); // Пропускаем строку "Mesh vertices"
// Считываем вершины for i := 0 to VertexCount - 1 do begin
Readln(f,Vertex.x,Vertex.y,Vertex.z); Vertices[i] := Vertex; end;
ReadLn(f, S); // Пропускаем строку "end vertices" ReadLn(f, S); // Пропускаем строку "Mesh faces"
// Считываем грани for i := 0 to FacesCount - 1 do begin Readln(f,Face[0],Face[1],Face[2]); Face[0] := Face[0] - 1; Face[1] := Face[1] - 1; Face[2] := Face[2] - 1; Faces[i] := Face; end;
CloseFile(f);
// Рассчитываем масштаб
MaxVertex := 0;
for i := 0 to VertexCount - 1 do begin MaxVertex := Max(MaxVertex,Vertices[i].x); MaxVertex := Max(MaxVertex,Vertices[i].y); MaxVertex := Max(MaxVertex,Vertices[i].z); end;
fExtent := 1/MaxVertex;
CalcNormals;
end;
Здесь могут быть непонятны следующие моменты: В блоке считывания граней я вычитаю единицу из каждого индекса вершины, считанного из файла. Делается это потому, что в программе индексы нумеруются, начиная с нуля, а в файле GMS - начиная с единицы. Процедура CalcNormals служит для расчета нормалей и взята из книги "OpenGL графика в проектах Delphi" Михаила Краснова. О том, что такое нормали и зачем они нужны я расскажу в разделах "Фасетные нормали" и "Сглаживающие нормали". procedure TGLMesh.Draw; var i : Integer; Face : TGLFace;
begin
if Extent then glScalef(fExtent,fExtent,fExtent);
for i := 0 to FacesCount - 1 do begin
glBegin(GL_TRIANGLES); Face := Faces[i]; glNormal3fv(@FasetNormals[i]); glVertex3fv(@Vertices[Face[0]]); glVertex3fv(@Vertices[Face[1]]); glVertex3fv(@Vertices[Face[2]]);
glEnd;
end;
end;
Здесь все понятно. Сначала, если установлен флаг масштабирования, устанавливается масштаб одинаковый по всем осям, затем в цикле рисуются треугольники. Перед началом рисования треугольника объявляется нормаль к нему. В качестве параметров передаются не конкретные значения, а указатели на них. destructor TGLMesh.Destroy; begin
FreeMem(Vertices,VertexCount*SizeOf(TGLVertex)); FreeMem(Faces,FacesCount*SizeOf(TGLFace)); FreeMem(FasetNormals,FacesCount*SizeOf(TGLVector));
end;
Здесь тоже все понятно, просто освобождается память, занятая объектом. Вызовы процедур загрузки и отрисовки объекта находятся в модуле frmMain и не представляют ничего интересного.
Загрузка фасетных нормалей из файла GMS
Что такое фасетная нормаль? Фасетная нормаль, это самая обычная нормаль к грани, а называется она так по производимому воздействию на изображаемый объект. После применения фасетных нормалей грани объекты хоть и освещены по-разному, но каждая грань освещена равномерно и соответственно закрашена одним цветом, что приводит к тому, что объект выглядит граненым. Отсюда и название. По-нашему "фасетная нормаль" это "граненая нормaль". В предыдущих примерах фасетные нормали рассчитывались по математическому алгоритму (процедура CalcNormals), но по всей видимости он иногда дает сбои. Не все то хорошо для программиста, что хорошо для математика. В результате и появляются черные треугольники там где их не должно быть.
К счастью, внутренний язык 3D Studio Max позволил мне найти фасетные нормали, которые он использовал для отображения объекта, а отображались объекты в 3D Studio Max правильно. Приложение, использующее нормали, взятые из 3D Studio Max, находится в папке Ch03. А какая при этом получается разница, Вы можете увидеть на картинках ниже:
Теперь наша баранка выглядит правильно. В процедуре загрузки сетки добавился блок считывания фасетных нормалей из файла GMS. Процедуру CalcNormals я оставил в исходном тексте, но закоментировал. ReadLn(f, S); //Пропускаем строку "end faces"
ReadLn(f, S); // Пропускаем строку "Faset normals"
// фасетные нормали
for i := 0 to FacesCount - 1 do begin
Readln(f,Normal.x,Normal.y,Normal.z); FasetNormals[i] := Normal;
end;
Естественно, что количество фасетных нормалей равняется количеству граней.
Загрузка сглаживающих нормалей из файла GMS
Все-таки, несмотря на то, что объект теперь отображается правильно, хочется чего-то еще. Ну кому понравится граненая баранка? Или футбольный мяч такой, будто его вытесали из гранита? И, несмотря на то, что уровень детализации в данном примере не высок, можно еще улучшить внешний вид объекта. На помощь приходят сглаживающие нормали. Об этом стоит рассказать подробнее.
Когда я понял, что, используя команду glShadeModel, мне не удастся сгладить мой объект (и у Вас не получится тоже), я затосковал. Нужно было что-то делать, и я решил заняться этим вопросом вплотную. Вот что мне удалось выяснить. Оказывается к одной грани можно построить не одну нормаль, а столько, сколько душа пожелает. Но это еще ничего не дает. А вот если мы нормаль отклоним в сторону, так что она станет, не перпендикулярна грани, то грань окрасится неравномерно. Конечно, слова о том, что "нормаль не перпендикулярна", могут показаться немного странными для математика, но программиста это смущать не должно :). Я попробую пояснить подробнее, что же получается в этом случае, на рисунках.
Взгляните на них. Как видно из рисунков, мы имеем четырехугольную грань, в каждом углу которой построена нормаль. На первом рисунке все нормали перпендикулярны грани, и грань выглядит плоской. На втором рисунке нормали разведены в стороны от центра грани и грань освещена неравномерно, так будто она выпукла, хотя на самом деле она плоская. Если же свести нормали к центру грани, то грань станет вогнутой.
Это можно применять следующим образом. Чтобы добиться эффекта сглаживания, строить нормали нужно к вершинам грани, на каждую вершину по одной нормали. Для построения нормали, необходимо узнать к каким граням принадлежит вершина (теоретически вершина может принадлежать бесконечному числу граней - на практике не больше 12), взять фасетные нормали от этих граней, расчитать от них среднюю нормаль и построить ее к вершине. Как это сделать? Какими формулами это считается? Честно говоря, я понятия не имею. Есть такой сайт: Ната Робинсона, там лежит пример на сглаживание и не только. Правда, написан он на Сях. Мне бы не составило труда переписать его на Дельфи, но... Зачем утруждать себя, если есть Баунти? Снова берем 3D Studio Max, лезем внутрь, хватаем сглаживающие нормали и... Вуаля!
Проект находится в папке Ch04. Скомпилируйте его и запустите на выполнение. Теперь Вы можете наслаждаться внешним видом сглаженного бублика нажав на кнопку Фасеты/Сгладить. Выглядит это примерно так:
Код программы, как всегда существенно не изменился. В процедуру загрузки добавился блок загрузки сглаживающих нормалей: ReadLn(f,S); // Пропускаем строку end faset normals
ReadLn(f,S); // Пропускаем строку SmoothNormals:
// Считываем сглаживающие нормали
for i := 0 to NextMesh.VertexCount - 1 do begin
Readln(f,Normal.x,Normal.y,Normal.z);
NextMesh.SmoothNormals[i] := Normal;
end;
Процедура отрисовки претерпела "существенные" изменения: procedure TGLMesh.Draw(Smooth: Boolean); var
i : Integer; Face : TGLFace;
begin
for i := 0 to FacesCount - 1 do begin
glBegin(GL_TRIANGLES);
Face := Faces[i];
if Smooth then begin
// Если сглаживать тогда перед каждой // вершиной рисуем сглаживающую нормаль glNormal3fv(@SmoothNormals[Face[0]]);
glVertex3fv(@Vertices[Face[0]]); glNormal3fv(@SmoothNormals[Face[1]]); glVertex3fv(@Vertices[Face[1]]); glNormal3fv(@SmoothNormals[Face[2]]); glVertex3fv(@Vertices[Face[2]]);
// Если не сглаживать один раз рисуем // фасетную нормаль end else begin
glNormal3fv(@FasetNormals[i]);
glVertex3fv(@Vertices[Face[0]]); glVertex3fv(@Vertices[Face[1]]); glVertex3fv(@Vertices[Face[2]]); end;
glEnd; end;
end;
procedure TGLMultyMesh.Draw; begin
if Extent then begin
fExtent := TGLMesh(Meshes.Items[CurrentFrame]) .fExtent;
glScalef(fExtent,fExtent,fExtent);
end;
TGLMesh(Meshes.Items[CurrentFrame]).Draw(fSmooth);
if Action then begin
inc(CurrentFrame);
if CurrentFrame > (Meshes.Count - 1) then CurrentFrame := 0;
end;
end;
Сам объект TGLMesh дополнился массивом для сглаживающих нормалей, а TGLMultyMesh - флагом указывающим следует ли сглаживать или нет. Этот флаг передается в процедуру отрисовки объекта TGLMesh. Деструктор пополнился строкой уничтожающей массив сглаживающих нормалей. В модуле frmMain появился обработчик нажатия пункта меню Фасеты/Сгладить.
Вот, пожалуй, и все. Могу только добавить, что не всегда удобно пользоваться сглаживающими нормалями из файла GMS, хотя в большинстве случаев они подходят. Загрузите, к примеру, объект Zban.gms и установите сглаживающий режим. Видите, все сглажено, а в 3D Studio Max он выглядел по-другому. Сверху и снизу у него были полукруглые крышки, но посередине был четкий цилиндр, с резкой границей в местах состыковки с полукруглыми крышками. Это побочный эффект сглаживания. Если Вы хотите добится исчезновения этого эффекта, Вам придется написать приложение для ручной корректировки нормалей, или программно отслеживать ситуацию, когда излом достиг критического угла и следует воспользоваться фасетной нормалью. Теперь, пожалуй, действительно все.
с этой утилитой Вам понадобится
Для ознакомления с этой утилитой Вам понадобится графический пакет 3D Studio Max 3.0 и, собственно, сама утилита. Она расположена в папке Utility и называется MEGA.ms. Это не исполняемый файл, а текстовый файл с набором команд для 3D Studio Max написанных на языке Max Script.
Как работает утилита: При экспорте файла, берется значение из поля From и ползунок счетчика кадров расположенный внизу экрана премещается на позицию, соответствующую этому значению. Затем в выходной файл экспортируется объект в том виде, в каком он пребывает на данный момент на экране. После чего снова передвигается ползунок кадров на величину, введенную в поле Step. Снова записывается модель соответствующая этому кадру. И так до тех пор, пока ползунок не переместится на позицию соответствующую значению, введенному в поле To. Поскольку в данном примере мы не создавали анимацию, то нам нужен был только один кадр. Утилита экспортировала кадр №1, затем добавила к нему значение 100. Номер кадра стал равен 101. Поскольку это значение больше значения введенного в поле To, процесс экспорта на этом остановился. Если бы в поле From было введено значение 0, то было бы экспортировано 2 кадра с номерами 0 и 100 соответственно. Если пометить галочкой опцию Selected Only, то экспортироваться будут только выделенные объекты, это иногда бывает очень нужно, в противном случае будут экспортированы все объекты сцены. Теперь рекомендую рассмотреть формат файла GMS.
Использование форм, созданных ранее
Если необходимо использовать формы, созданные ранее, то нет необходимости переписывать их заново. Необходимо проделать следующие операции:
в модуле PAS необходимо заменить конструкцию TOldForm = class(TForm) на TOldForm = class(TfrmParent)
в модуле DFM необходимо заменить конструкцию object OldForm: TOldForm на inherited OldForm: TOldForm
написать свои обработчики методов.
Скачать пример: (6К)
Создание базовой формы
Для начала определим типы кнопок, которые необходимо обрабатывать. Для этого создадим тип TPrButton как набор.
Далее воспользуемся механизмом наследования и создадим базовую форму, у которой определим:
виртуальную функцию ButtonUpdate, переназначение которой позволит наследуемой форме обрабатывать событие "Активна та или иная кнопка или нет". Параметром данной функции служит тип обрабатываемой кнопки. Если функция возвращает True, то кнопка активна, иначе - неактивна. По умолчанию всегда возвращается False.
виртуальную процедуру ButtonExecute, предназначение которой обработать событие нажатия кнопки. Параметром передается тип обрабатываемой кнопки.
На основе выше изложенного имеем:
type TPrButton = (prPrint, prPreview, prExport);
TfrmParent = class(TForm) procedure FormClose(Sender: TObject; var Action: TCloseAction); private { Private declarations }
public { Public declarations }
function ButtonUpdate(PrButton: TPrButton): Boolean; virtual; procedure ButtonExecute(PrButton: TPrButton); virtual; end; |
Создание главной формы
Займемся созданием главной формы. Главная форма должна предоставляить механизм обработки набора кнопок. Для этого на главной форме создадим список операций (ActionList) и определим 3 операции (ActionItem): ActionPrint, ActionPreview, ActionExport. Создадим панель инструментов с набором кнопок: Печать, Предварительный просмотр и Экспорт и назначим этим кнопки соответствующие операции. Для каждой операции назначим один и тот же метод обработки событий OnUpdate
procedure TfrmMain.ActionPrButtonUpdate(Sender: TObject); var m: TPrButton; begin TAction(Sender).Enabled := False;
if Sender = ActionPrint then m := prPrint else if Sender = ActionPreview then m := prPreview else if Sender = ActionExport then m := prExport else Exit;
try TAction(Sender).Enabled := Assigned(Application. MainForm.ActiveMDIChild) and (Application.MainForm.Active MDIChild is TfrmParent) and TfrmParent(Application. MainForm.ActiveMDIChild).ButtonUpdate(m); except TAction(Sender).Enabled := False; end; // try
Application.ProcessMessages; end;
и OnExecute
procedure TfrmMain.ActionPrButtonExecute(Sender: TObject); var m: TPrButton; begin if Not Assigned(Application.MainForm.ActiveMDIChild) or Not (Application.MainForm.ActiveMDIChild is TfrmParent) then Exit;
if Sender = ActionPrint then m := prPrint else if Sender = ActionPreview then m := prPreview else if Sender = ActionExport then m := prExport else Exit;
TfrmParent(Application.MainForm.ActiveMDIChild). ButtonExecute(m); end; |
Создание наследуемой формы
Для создания наследуемой формы воспользуемся следующим механизмом:
Выбираем в меню "File" -> "New..."
В форме "New Itmes" переходим на вкладку с названием нашего проекта (в примере Project1)
В списке форм выбираем базовую форму (frmParent) и нажимаем Ok.
В итоге создается новый модуль.
Далее необходимо написать собственные обработчики.
Рассмотрим пример на основе формы frmWindow1. Переписываем методы ButtonUpdate и ButtonExecute.
type TfrmWindow1 = class(TfrmParent) ... public function ButtonUpdate(PrButton: TPrButton): Boolean; override; procedure ButtonExecute(PrButton: TPrButton); override; end; |
и создаем собственные обработчики методов:
function TfrmWindow1.ButtonUpdate(PrButton: TPrButton): Boolean; begin Result := (PrButton = prPrint) and cbCanPrint. Checked or (PrButton = prPreview) and cbCanPreview. Checked or (PrButton = prExport) and cbCanExport. Checked; end;
procedure TfrmWindow1.ButtonExecute(PrButton: TPrButton); begin case PrButton of prPrint: ShowMessage( Caption + ' Print'); prPreview: ShowMessage( Caption + ' Preview'); prExport: ShowMessage( Caption + ' Export'); end; end; |
Таким образом при создании нескольких окон класса TfrmWindow1 для каждого окна будет свой обработчик событий.
с множеством документов. Каждый документ
Предположим вы создаете приложение с множеством документов. Каждый документ должен обладать возможностью отправить на печать свое содержимое или предоставить возможность предварительного просмотра его печатной формы на экране или предоставить возможность сохранить данные во внешний файл. Таким образом имеем на основной форме набор из трех кнопок: Печать, Промотр, Экспорт. Каждая форма должна обрабатывать запросы: активировать кнопку и выполнить действие для этой кнопки.
Благодарности.
Хочу поблагодарить свою супругу за любезно предоставленный компьютер для тестирования своих параллельных приложений.
Также выношу благодарность Шихалеву Ивану, который сильно помог в исправлении неточностей и ошибок первоначальной версии модуля mpi.pp.
Более сложные программы.
Сейчас, когда заработала простейшая программа, можно начать осваивать функции обмена данными — именно то, что позволяет осуществить взаимодействие между процессами.
Функции двухточечного обмена.
Блокирующая передача (прием) — означает, что программа приостанавливает свое выполнение, до тех пор, пока передача (прием) не завершится. Это гарантирует именно тот порядок выполнения операций передачи (приема), который задан в программе.
Блокирующая передача осуществляется с помощью функции MPI_Send. function MPI_Send( buf : pointer; count : longint; datatype : MPI_Datatype; destination : longint; tag : longint; comm : MPI_Comm) : longint;
Осуществляет передачу count элементов указанного типа процессу под номером destination.
buf | — адрес первого элемента в буфере передачи |
count | — количество передаваемых элементов в буфере |
datatype | — MPI-тип этих элементов |
destination | — ранг процесса-получателя (принимает значения от нуля до n-1, где n — полное число процессов) |
tag | — тег сообщения |
comm | — коммуникатор |
В качестве MPI-типа следует указать один из нижеперечисленных типов. Большинству базовых типов паскаля соответствует свой MPI-тип. Все они перечислены в следующей таблице. Последний столбец указывает на число байт, требуемых для хранения одной переменной соответствующего типа.
MPI_CHAR |
shortint | 1 |
MPI_SHORT |
smallint | 2 |
MPI_INT |
longint | 4 |
MPI_LONG |
longint | 4 |
MPI_UNSIGNED_CHAR |
byte | 1 |
MPI_UNSIGNED_SHORT |
word | 2 |
MPI_UNSIGNED |
longword | 4 |
MPI_UNSIGNED_LONG |
longword | 4 |
MPI_FLOAT |
single | 4 |
MPI_DOUBLE |
double | 8 |
MPI_LONG_DOUBLE |
double | 8 |
MPI_BYTE |
untyped data | 1 |
MPI_PACKED | составной тип | - |
Переменная tag — вспомогательная целочисленная переменная.
MPI-тип MPI_PACKED используется при передаче данных производных типов (сконструированных из базовых типов). Их рассмотрение выходит за рамки данной статьи.
Функция MPI_Recv реализует блокирующий прием данных. function MPI_Recv( buf : pointer; count : longint; datatype : MPI_Datatype; source : longint; tag : longint; comm : MPI_Comm; var status : MPI_Status) : longint;
buf | — начальный адрес буфера приема |
count | — максимальное количество принимаемых элементов в буфере |
datatype | — MPI-тип этих элементов |
source | — ранг источника |
tag | — тег сообщения |
comm | — коммуникатор |
status | — статус обмена |
Эта функция осуществляет запрос на получение данных. При ее вызове процесс будет ожидать поступления данных от процесса под номером source. Если таковой не последует, то это приведет к повисанию программы (тупик). Так что при использовании этих функций следует проявлять бдительность.
Число принятых элементов может быть меньше значения переменной count. Если же посылаемые данные имеют больший размер, то будет выведено предупреждение об обрывании передачи.
Возвращаемая переменная status содержит информацию о передаче. Например, ее можно использовать, чтобы определить фактическое количество принятых элементов. Для этого используется функция MPI_Get_count function MPI_Get_count(var status : MPI_Status; datatype : MPI_Datatype; var count : longint) : longint;
Число фактически принятых элементов — в возвращаемой переменной count.
Функции коллективного обмена.
Коллективный обмен данными затрагивает не два процесса, а все процессы внутри коммуникатора.
Простейшими (и наиболее часто используемыми) разновидностями такого вида взаимодействия процессов являются рассылка MPI_Bcast и коллективный сбор данных MPI_Reduce. function MPI_Bcast( buff : pointer; count : longint; datatype : MPI_Datatype; root : longint; comm : MPI_Comm) : longint;
buf | — адрес первого элемента буфера передачи |
count | — максимальное количество принимаемых элементов в буфере |
datatype | — MPI-тип этих элементов |
root | — ранг источника рассылки |
comm | — коммуникатор |
Функция MPI_Bcast реализует "широковещательную передачу". Один процесс ( главный или root процесс) рассылает всем (и себе, в том числе) сообщение длины count, а остальные получают это сообщение. function MPI_Reduce( buf : pointer; result : pointer; count : longint; datatype : MPI_Datatype; operation : MPI_Op; root : longint; comm : MPI_Comm) : longint;
buf | — адрес первого элемента буфера передачи |
count | — количество элементов в буфере передачи |
datatype | — MPI-тип этих элементов |
operation | — операция приведения |
root | — ранг главного процесса |
comm | — коммуникатор |
Функция MPI_Reduce выполняет операцию приведения над массивов данных buf, полученным от всех процессов, и пересылает результат в result одному процессу (ранг которого определен параметром root).
Как и функция MPI_Bcast, эта функция должна вызываться всеми процессами в заданном коммуникаторе, и аргументы count, datatype и
operation должны совпадать.
Имеется 12 предопределенных операций приведения
MPI_MAX | максимальное значение |
MPI_MIN | минимальное значение |
MPI_SUM | суммарное значение |
MPI_PROD | значение произведения всех элементов |
MPI_LAND | логическое "и" |
MPI_BAND | побитовое "и" |
MPI_LOR | логическое "или" |
MPI_BOR | побитовое "или" |
MPI_LXOR | логическое исключающее "или" |
MPI_BXOR | побитовое исключающее "или" |
MPI_MAXLOC | индекс максимального элемента |
MPI_MINLOC | индекс минимального элемента |
Использование функций двухточечного обмена.
В следующем примере вычисление значений элементов массива "разводится" по двум процессам uses mpi; const num = 10; var
teg, numprocs, myid : longint; i : longint; status : MPI_Status; z, x : double; arr : array[0..num] of double; function f( x : double) : double; begin
f := sqr(x); end; begin
MPI_Init(argc,argv); teg := 0; MPI_Comm_size(MPI_COMM_WORLD, numprocs); MPI_Comm_rank(MPI_COMM_WORLD, myid); for i := 0 to num do
case myid of
0: if i mod 2 = 0 then arr[i] := f(1.0*i) else
begin
MPI_Recv(@x,1,MPI_DOUBLE,1,teg,MPI_COMM_WORLD,status); arr[i] := x end; 1: if i mod 2 = 1 then
begin
z := f(1.0*i); MPI_Send(@z,1,MPI_DOUBLE,0,teg,MPI_COMM_WORLD); end; end; // case statement
if myid = 0 then for i := 0 to num do writeln(i,' ',arr[i]); MPI_Finalize; end.
Формируется массив заданного числа элементов так, что элементы с четными номерами рассчитывает процесс с myid=0, а нечетными — с myid=1. Конечно, вместо функции sqr может стоять любая другая. Программа написана, конечно же, в расчете на то, что процессов будет всего два. Поскольку значения myid, отличные от 0 и 1, не используются, процессы с такими номерами будут простаивать.
Улучшить программу, то есть написать такой ее вариант, чтобы использовались все процессы, предоставляю читателю :)
Использование коллективных функций ( вычисление числа ?).
Следующая программа демонстрирует вычисление определенного интеграла.
if myid=0 then
begin
Assign(fname,'n.in'); {$I-}
Reset(fname); Readln(fname,n); Close(fname); {$I+}
startwtime := MPI_Wtime; end;
MPI_Bcast( @n, 1, MPI_INT, 0, MPI_COMM_WORLD);
if n<>0 then
begin
h := 1.0/n; sum := 0.0; i := myid + 1; while i <= n do
begin
x := h*( i - 0.5); sum := sum + f(x); i := i + numprocs; end; mypi := h*sum; MPI_Reduce( @mypi, @pimy, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);
if myid = 0 then
begin
writeln('; error is', abs(pimy-pi)); endwtime := MPI_WTime; writeln('wall clock ', endwtime-startwtime) end;
end; MPI_Finalize; end.
Файл n.in, содержащий в первой строке число разбиений (чем больше число, тем точнее считается ?) должен присутствовать в том каталоге, где находится исполняемый файл.
Обратите внимание на то, что в этой программе нет
case-вилок &mdash все процессы вызывают одни и те же функции.
Полезная функция MPI_Wtimefunction MPI_Wtime : double;
возвращает время ( в секундах), прошедшее с некоторого фиксированного момента в прошлом. Гарантируется, что этот фиксированный момент неизменен в течение работы процесса. С помощью этой функции можно отслеживать время вычислений и оптимизировать распараллеливание программы.
В каталоге
SDK/Examples также можно найти файл systest.c.
Здесь находится версия этой программы, написанная на паскале.
Модуль mpi на FreePascal.
Все вышеописанное относилось к установке собственно MPICH. Для того, чтобы прикрутить библиотеки MPICH к FreePascal, следует еще немножко поработать.
Cледует воспользоваться динамической библиотекой mpich.dll, которая располагается в системном каталоге (копируется туда при установке MPICH).
1. Скачать модуль FreePascal, реализующий функции этой динамической библиотеки. Файл mpi.pp
2. Для использования модуля mpi следует просто скопировать файл mpi.pp в каталог, где FreePascal ищет модули (unit searchpath).
Модуль написан с использованием утилиты h4pas.exe и заголовочных файлов *.h из
SDK\Include.
Настройка
Настройку можно осуществить с помощью простых утилит, имеющихся в дистрибутиве.
Остановимся подробнее на каталоге mpd\bin в директории MPICH. Содержимое каталога:
mpd.exe |
исполняемый файл службы mpich_mpd |
нужна |
MPIRun.exe |
файл, осуществляющий запуск каждой MPI-программы. |
нужна |
MPIRegister.exe |
программа для шифрования паролей при обмене данными по LAN. |
иногда полезна |
MPDUpdate.exe |
программа для обновления библиотек MPI |
не нужна |
MPIConfig.exe |
программа настройки хостов в кластере |
не нужна |
guiMPIRun.exe |
GUI версия mpirun. |
не нужна |
MPIJob.exe |
программа для управления MPI-процессами |
не нужна |
guiMPIJob.exe |
GUI версия mpijob.exe |
не нужна |
Использование команд mpirun и mpiregister ждет нас впереди. Чтобы удостовериться, что службы MPICH, работающие на разных компьютерах, взаимодействуют должным образом, можно воспользоваться утилитой MPIconfig. Для этого следует
1. Запустить
MPIConfig.exe (можно воспользоваться ссылкой в главном меню, она там должна быть)
2. Нажать на кнопку "Select"
3. В появившемся окне выбрать пункт меню "Action"—"Scan hosts"
4. Напротив имени каждой машины должна загореться пиктограмма "MPI" ( примерно вот так)
Основные функции.
Основные функции MPI, с помощью которых можно организовать параллельное вычисление
1 |
MPI_Init |
подключение к MPI |
2 |
MPI_Finalize |
завершение работы с MPI |
3 |
MPI_Comm_size |
определение размера области взаимодействия |
4 |
MPI_Comm_rank |
определение номера процесса |
5 |
MPI_Send |
стандартная блокирующая передача |
6 |
MPI_Recv |
блокирующий прием |
Утверждается, что этого хватит. Причем первые четыре функции должны вызываться только один раз, а собственно взаимодействие процессов — это последние два пункта.
Описание функций, осуществляющих передачу, оставим на потом, а сейчас рассмотрим описание функций инициализации/завершения function MPI_Init( var argc : longint; var argv : ppchar) : longint;
Инициализация MPI. Аргументы argc и argv — переменные модуля system, определяющие число параметров командной строки и сами эти параметры, соответственно.
При успешном вызове функции MPI_Init создается
коммуникатор ( область взаимодействия процессов), под именем MPI_COMM_WORLD. function MPI_Comm_size( comm : MPI_Comm; var nump : longint) : longint;
Определяет число процессов, входящих в коммуникатор comm. function MPI_Comm_rank( comm : MPI_Comm; var proc_id : longint) : longint;
Определяется ранг процесса внутри коммуникатора. После вызова этой функции все процессы, запущенные загрузчиком MPI-приложения, получают свой уникальный номер (значение возвращаемой переменной proc_id у всех разное). После вызова функции MPI_Comm_rank можно, таким образом, назначать различным процессам различные вычисления. functionnn MPI_Finalize : longint;
Завершает работу с MPI.
Порядок вызова таков:
1. MPI_Init — подключение к MPI
2. MPI_Comm_size — определение размера области взаимодействия
3. MPI_Comm_rank — определение номера процесса
4. Далее идет любая совокупность команд обмена (передача, прием, и тп.)
5. MPI_Finalize — завершение работы с MPI
Простейшая MPI программа такова.
test.pas
uses mpi; var namelen, numprocs, myid : longint; processor_name : pchar;
begin
MPI_Init( argc, argv); MPI_Comm_size( MPI_COMM_WORLD, numprocs); MPI_Comm_rank( MPI_COMM_WORLD, myid); GetMem( processor_name, MPI_MAX_PROCESSOR_NAME+1); // константа MPI_MAX_PROCESSOR_NAME равна 256
namelen := MPI_MAX_PROCESSOR_NAME; MPI_Get_processor_name( processor_name, namelen); Writeln('Hello from ',myid,' on ', processor_name); FreeMem(processor_name); MPI_Finalize; end.
Здесь, как видно, никакого обмена нет, каждый процесс только "докладывает" свой ранг.
Для наглядности выводится также имя компьютера, где запущен каждый процесс. Для его определения используется функция MPI_Get_processor_name. function MPI_Get_processor_name( proc_name : Pchar; var name_len : longint) : longint;
При успешном вызове этой функции переменная proc_name содержит строку с именем компьютера, а name_len — длину этой строки.
После компиляции (с соответствующими опциями) >fpc -dRELEASE [-Fu<каталог, где размещен файл mpi.pp>] test.pas
должен появиться исполняемый файл test.exe, однако рано радоваться. Запуск этого exe-файла не есть запуск параллельной программы.
Полезные ссылки.
1. http://www.mpi-forum.org/ — сайт, посвященный стандарту MPI.
2. http://www-unix.mcs.anl.gov/ — официальный сайт MPICH.
3. http://www.parallel.ru/ — ведущий русскоязычный сайт по параллельным вычислениям. На форуме будьте осторожны — там люди программируют на Си !
4.
http://www.parallel.uran.ru/doc/mpi_tutor.html — хороший учебник по MPI для начинающих.
Не могу не порекомендовать также и печатную литературу по этой тематике:
1. С. Немнюгин, О. Стесик. Параллельное программирование для многопроцессорных вычислительных систем. "БХВ-Петербург" СПб, 2002.
Основы параллельного программирования изложены в доступной форме, большую часть книги занимает именно описание функций библиотеки MPI.
2. В.Д. Корнеев. Параллельное программирование в MPI. "Институт компьютерных исследований" М, Ижевск, 2003.
Здесь изложение гораздо более "приземленное", что тоже хорошо, так как описываются (и снабжаются кодом на Си) конкретные алгоритмы, использующие параллельные вычисления.
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 |
Внимание! Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав. |
Французские оптом и в розницу.
|
Простейшая MPI программа на FreePascal.
Во именах всех функциях библиотеки MPICH используется префикс MPI_. Возвращаемое значение большинства функций — 0, если вызов был успешным, а иначе — код ошибки.
Установка.
Компьютеры, участвующие в вычислениях, назовем кластером. MPICH должен быть установлен на каждом компьютере в кластере.
Для установки нужно
1. Скачать
(5278 Кб) или (5248 Кб)
Либо с официальной страницы MPICH
http://www.mcs.anl.gov/mpi/mpich/download.html
Либо с ftp сервера
ftp.mcs.anl.gov/pub/mpi/nt.
2. Если запустить exe файл, то после распаковки запустится интерактивная программа установки MPICH. Чтобы не утомлять себя выбором устанавливаемых компонент, удобнее установить MPICH в неинтерактивном режиме.
Для этого
а. Разархивируйте содержимое в общую папку (например, \\ILYA\common)
b. Отредактируйте файл setup.iss
c. Строка szDir=C:\Program Files\MPICH
определяет каталог, куда установится MPICH. Это расположение можно изменить.
d. Строки Component-count=7 Component-0=runtime dlls Component-1=mpd Component-2=SDK Component-3=Help Component-4=SDK.gcc Component-5=RemoteShell Component-6=Jumpshot
определяют число устанавливаемых компонент. Для главного компьютера (откуда запускается главный процесс) подходящие опции таковы Component-count=4 Component-0=runtime dlls Component-1=mpd Component-2=SDK Component-3=Help
Для простого компьютера (которому отводится только роль вычислителя) число компонент может быть сокращено до двух. Component-count=2 Component-0=runtime dlls Component-1=mpd
На каждом компьютере кластера выполнить команду установки в неинтерактивном режиме. В моем случае запуск программы установки таков: >\\ILYA\common\setup -s -f1\\ILYA\common\setup.iss
После установки на каждом компьютере должна запуститься служба mpich_mpd (MPICH Daemon (C) 2001 Argonne National Lab). (смотрите рисунок)
Если был установлен компонент SDK (что необходимо сделать на том компьютере, откуда будет производиться запуск программ), то в каталоге MPICH (прописанном в пункте szDir) присутствуют подкаталоги SDK и SDK.gcc. Содержимое этих каталогов — библиотечные и заголовочные файлы для языков C, С++ и Fortran.
Каталог SDK предназначен для компиляторов MS VC++ 6.x и Compaq Visual Fortran 6.x, а каталог SDK.gcc — для компиляторов gcc и g77.
Установка и настройка MPICH.
MPICH для Windows требует
1. Windows NT4/2000/XP ( Professional или Server). Под Win9x/ME работать не станет!
2. Сетевое соединение по протоколу TCP/IP между машинами.
Сразу обговорю, что все примеры тестировались на двух машинах, объединенных в локальную сеть. Один компьютер (сетевое имя ILYA) — мой, а второй (сетевое имя
EKATERINA) — жены.
Утилита MPIRegister.exe.
Поскольку компьютеры ILYA и EKATERINA объединены в локальную сеть, у меня нет никаких проблем с безопасностью. Пароль для пользователя mpiuser хранится в открытом виде в файле lgn. Увы, так можно делать далеко не всегда. Если компьютеры, входящие в кластер, являются частью более разветвленной сети, или, более того, используют подключение к Internet, так поступать не просто не желательно, а недопустимо.
В таких случаях следует хранить пароль пользователя, от имени которого будут запускаться процессы, в системном реестре Windows в зашифрованном виде. Для этого предназначена программа MPIRegister.exe.
Опции таковы
mpiregister |
Запрашивает имя пользователя и пароль (дважды). После ввода спрашивает, сделать ли установки постоянными. При ответе 'yes' данные будут сохранены на диске, а иначе — останутся в оперативной памяти и при перезагрузке будут утеряны. |
mpiregister -remove |
Удаляет данные о пользователе и пароле. |
mpiregister -validate |
Проверяет правильность сохраненных данных. |
Запускать
mpiregister следует только на главном компьютере. Загрузчик приложения
mpirun без опции -pwdfile будет запрашивать данные, сохраненные программой mpiregister. Если таковых не обнаружит, то запросит имя пользователя и пароль сам.
О чем эта статья.
Статья посвящена вопросу написания распределенных (параллельных) вычислений с использованием компилятора FreePascal (использовалась версия 2.0.1)
Проблема параллельных вычислений заинтересовала меня совсем не потому что это сейчас модно. Столкнулся с задачей, когда надо было сформировать (для дальнейнего анализа) большой массив данных. Хотелось уменьшить время вычислений имеющимися средствами. Оказывается, организовать параллельные вычисления с использованием моего любимого компилятора — вполне решаемая задача.
Стандартом для параллельных приложений для многопроцессорных вычислительных систем де-факто является MPI.
Идея MPI-программы такова: параллельная программа представляется в виде множества взаимодействующих (посредством коммуникационных процедур MPI) процессов.
Параллельные вычисления требуют
1. Разделения процессов
2. Взаимодействия между ними
MPI (Message Passing Interface) — стандарт на программный инструментарий для обеспечения связи между ветвями параллельного приложения.
В этой статье рассматривается MPICH (MPI CHameleon), свободно распространяемая реализация MPI. Использовалась версия MPICH 1.2.5 для Windows.
pp содержит описание 230 функций
Модуль
mpi. pp содержит описание 230 функций MPI. У меня нет никакой возможности перечислить их все, да я и не ставил перед собой такой задачи. Я могу лишь гарантировать, что все функции, которые я использовал в приведенных примерах, работают правильно.
Если же Вам удалось найти (а еще лучше &mdash исправить) какой-либо баг в файле mpi.pp &mdash большая просьба сообщить об этом мне на .
Замеченные мною баги:
1. Функции MPI_Info_c2f, MPI_Info_f2c и MPI_Request_c2f
Что они делают, я не знаю. В текущем модуле mpi.pp эти функции остаются нереализованными.
Запуск MPI-программы.
Запуск MPI-программы осуществляется с помощью загрузчика приложения mpirun. Формат вызова таков: >mpirun [ключи mpirun] программа [ключи программы]
Вот некоторые из опций команды mpirun:
-np x |
запуск x процессов. Значение x может не совпадать с числом компьютеров в кластере. В этом случае на некоторых машинах запустится несколько процессов. То, как они будут распределены, mpirun решит сам (зависит от установок, сделанных программой
MPIConfig.exe) |
-localonly x |
-np x -localonly |
запуск x процессов только на локальной машине |
-machinefile filename |
использовать файл с именами машин |
-hosts n host1 host2 ... hostn |
-hosts n host1 m1 host2 m2 ... hostn mn |
запустить на n явно указанных машинах. Если при этом явно указать число процессов на каждой из машин, то опция -np становится необязательной |
-map drive: \\host\share |
использовать временный диск |
-dir drive:\my\working\directory |
запускать процессы в указанной директории |
-env "var1=val1|var2=val2|var3=val3..." |
присвоить значения переменным окружения |
-logon |
запросить имя пользователя и пароль |
-pwdfile filename |
использовать указанный файл для считывания имени пользователя и пароля.
Первая строка в файле должна содержать имя пользователя, а вторая — его пароль) |
-nocolor |
подавить вывод от процессов различным цветом |
-priority class[:level] |
установить класс приоритета процессов и, опционально, уровень приоритета.
class = 0,1,2,3,4 = idle, below, normal, above, high
level = 0,1,2,3,4,5 = idle, lowest, below, normal, above, highest |
по умолчанию используется -priority 1:3, то есть очень низкий приоритет. |
Для организации параллельного вычисления на нескольких машинах следует
1. На каждом компьютере, входящем в кластер, завести пользователя с одним и тем же именем (например, MPIUSER) и паролем (я дал ему пароль "1"), с ограниченными привилегиями.
2. На главном компьютере (в моем случае это, разумеется, ILYA) создать сетевую папку (например, COMMON). Следует озаботиться, чтобы пользователь MPIUSER имел к ней полный доступ.
3. В той же папке создать файл, содержащий имя пользователя, от чьего имени будут запускаться процессы, а также его пароль. В моем случае содержимое этого файла должно быть таким: mpiuser 1Я назвал это файл lgn.
После всех этих действий запуск MPI программы test осуществить можно как >mpirun -pwdfile \\ILYA\COMMON\lgn -hosts 2 ILYA 1 EKATERINA 1 \\ILYA\COMMON\test.exe
Изменив соответствующие опции, можно запускать различное число процессов. Например >mpirun -pwdfile \\ILYA\COMMON\lgn -hosts 2 ILYA 3 EKATERINA 3 \\ILYA\COMMON\test.exe
На рисунке виден результат такого вызова. Вывод от различных процессов выделяется различным цветом, поскольку опция -nocolor отключена. Обратите внимание на то, что последовательность номер выводимой строки вовсе не совпадает с номером процесса. Этот порядок будет меняться от случая к случаю.
На этом рисунке запечатлен Диспетчер задач при запуске на компьютере EKATERINA четырех процессов. Установлен приоритет по умолчанию.
Будьте недоверчивы
Очень часто алгоритмы кодируются из расчета на "нормальный" режим работы (достаточно ресурсов, присутствуют все необходимые компоненты, пользователи нажимают правильные комбинации клавиш и т.д.). Такие реализации очень плохо справляются с возникающими возмущениями. Во избежание этих проблем следуйте следующим простым правилам:
Проверяйте значения переменных на допустимость. Особенно это касается переменных типа указатель, процедурных переменных и объектов.
Защищайте пары выделение-освобождение ресурсов блоками try/finally. Предполагайте, что исключение может произойти в любом операторе.
Используйте процедуру Assert для проверки условий, которые всегда должны быть истинными.
Объем кода, добавленный для проверок и обработки ошибок, может достигать порядка "полезного" кода! Но, такой стиль программирования является необходимым условием при написании сложных систем. Что поделаешь, из бревен небоскреб не построишь
Частота выделения-освобождения ресурсов
Очевидно, что скорость потери ресурсов (памяти, дескрипторов и т.д.) пропорциональна частоте их выделения. Рассмотрите варианты реализации, в которых ресурсы выделяются наиболее редко. Таким образом, вы сможете отсрочить крах программы, и некоторые пользователи могут даже и не узнать, что с ней что-то не так.
Пример:
Допустим, в объекте есть метод DoSomething. В процессе работы он выделяет и освобождает память, которая нужна только ему. С точки зрения "выделения ресурсов по месту их использования" - все корректно, но при многократном обращении к этому методу и в случае наличия ошибки при освобождении памяти вы можете получить достаточно интенсивную утечку памяти. В данной ситуации имеет смысл рассмотреть одноразовое выделение памяти при создании объекта и освобождении при его разрушении. В данной ситуации при наличии ошибки скорость утечки будет гораздо меньше. Естественно, что данное решение необходимо рассматривать в комплексе с другими задачами (производительность, минимизация расхода ресурсов и т.д.)
Циклические ссылки модулей и "осведомленность" сущностей
Технически, Object Pascal позволят создать циклические ссылки между модулями. Их наличие в программе или библиотеке свидетельствует о не очень удачной декомпозиции (IMHO). Негативными последствиями их использования есть:
Вероятность, что в ваш проект будут включены неиспользуемые модули;
Сложная логика взаимодействия компонент, что усложняет поддержку и развитие проекта, а так же может быть источником функциональных ошибок.
Правило:
Избегайте использования циклических ссылок модулей. Старайтесь организовать "осведомленность" сущностей древовидной (сущности верхнего уровня знают о существовании сущностей нижнего уровня, но не наоборот). Обратное взаимодействие можно реализовывать посредством механизма событий (процедурных переменных) или при помощи сущностей "посредников".
Пример
Предположим, мы разрабатываем приложение, в котором должны производиться некоторые вычисления и процесс этих вычислений, должен представляться пользователю. Грубо говоря, в данной ситуации мы имеем две сущности: "интерфейс пользователя" и "вычислительный механизм". Они оба должны взаимодействовать друг с другом: "интерфейс пользователя" должен настраивать "вычислитель" и запускать его на выполнение, а "вычислитель" должен выдавать информацию о ходе расчетов. Можно предложить следующий вариант решения:
Модули интерфейса пользователя и вычислителя работают непосредственно друг с другом, т.е. "интерфейс пользователя" вызываем методы "вычислителя" и наоборот. Все будет работать великолепно, пока не окажется, что "вычислитель" необходимо использовать в другой задаче с другим интерфейсом пользователя (или без оного вообще). Обойти данную проблему можно, если в "вычислителе" задачу общения с "внешним миром" (в данном случае - интерфейс пользователя) возложить на функции обратного вызова (callback functions). При таком подходе, заинтересованная сторона регистрируется у "вычислителя", и он будет вызывать ее функции, не подозревая, с кем имеет дело.
Анализ:
В первом случае мы имели двунаправленную "осведомленность" сущностей друг о друге, что привело к проблемам с повторным использованием кода "вычислителя". Во втором случае у нас только однонаправленная "осведомленность" сущностей, т.е. "интерфейс пользователя" знает о вычислителе, но не наоборот. Если необходимо повторно использовать код "интерфейса пользователя", можно пойти дальше - сущность "приложение" знает о существовании сущностей "интерфейс пользователя" и "вычислитель", но последние ничего не знают друг о друге и взаимодействуют через сущность "приложение", исполняющую роль посредника.
Функции, процедуры и состояния
Для начала словарь терминов:
Функция - это подпрограмма, задачей которой является получение (извлечение, вычисление и т.д.) определенного значения на основании входных параметров и текущего состояния системы. Процедура - это подпрограмма, которая предназначена для выполнения каких либо действий над системой, и соответственно изменяет состояние системы.
Просьба не путать эти определения с ключевыми словами function и procedure.
Правило:
Подпрограмма должна быть либо функцией, либо процедурой. Не совмещайте эти две задачи в одной подпрограмме, разделите ее на несколько подпрограмм.
Инициализация переменных и полей
Неинициализированные переменные часто становятся причинами возникновения ошибок-фантомов. Обычно они имеют нерегулярную природу, и их трудно выявить в процессе отладки. Особенно катастрофичными могут быть последствия при таком обращении с указателями.
Правило:
Для глобальных переменных: использовать типизированные константы, инициализированные переменные или присваивать начальные значения переменным в секции инициализации модуля.
Для локальных переменных: присваивать начальные значения в первых строках процедуры или функции.
Для полей объектов: присваивать начальные значения полям в конструкторе и не полагаться на то, что память, выделенная под объект, инициализируется нулями.
Массивы, записи и выделенные блоки памяти очень удобно инициализировать при помощи функции FillChar. Но, с появлением в Delphi "управляемых" (manageable) типов (длинные строки, динамические массивы, варианты и интерфейсы), пользоваться ей необходимо с четким пониманием.
Пример
type TStrArray = array[1..10] of string; var A : TStrArray; ... FillChar(A, SizeOf(A), 0); |
В данном примере вызов процедуры FillChar проинициализирует строки пустыми значениями, такой подход был нормальным в ранних версиях Delphi и Borland Pascal, но недопустим в последних версиях, в которых тип string по умолчанию соответствует типу LongString и суть указатель. Если значения строк перед инициализацией были не пусты, то мы получим утечку памяти.
Интерфейсы объектов
Четко специфицируйте, какие методы, свойства и поля могут быть доступны и каким образом. "Прячьте" методы, свойства и поля, которые не должны быть доступны извне. Не давайте возможность пользоваться "недокументированными" возможностями ваших объектов. Если по каким либо причинам скрыть эти элементы не получается (к сожалению, система прав доступа к элементам объекта в Delphi несовершенна), тогда не забудьте оформить соответствующий комментарий.
Исключения в обработчике события OnTimer
При написания обработчика события OnTimer компонента TTimer необходимо учитывать, что возникновение исключения в нем для обычного Delphi приложения без специализированной обработки исключений приведет к выскакиванию диалога с сообщением об ошибке. Но это не останавливает работу таймера. И если причина возникновения исключения устойчива, то скоро вы увидите следующее сообщения и т.д., пока у системы не закончатся какие-нибудь ресурсы.
Решить данную проблему можно несколькими способами:
Обрабатывать исключения непосредственно в обработчике события try except on E: Exception do Application.ShowException(E); end;
Использовать централизованный обработчик исключений, который фиксирует их в протоколе или журнале, но не выдает никаких сообщений об ошибке. Application.OnException := MyExceptionHandler;
Последний подход желательно использовать в серверных приложениях, когда нет пользователя, который интерактивно взаимодействует с приложением.
Использование констант
Используйте именованные константы. Это увеличивает "настраиваемость" исходного кода. А также избавляет от проблем связанных с изменением значения константы в случае ее множественного вхождения.
Контроль достижения предела
Довольно часто встречаются случаи, когда контроль достижения предела цикла осуществляется условием равенства.
Пример
Repeat ... Inc(I); Until I = Limit;
|
Что произойдет, если в результате ошибки (или просто модификации алгоритма) переменная I перескочит через значение Limit? Правильно - ничего хорошего. Более устойчивой будет конструкция с использованием условия отсечения диапазона, т.е. I >= Limit.
Область использования переменных
Много сказано и написано на эту тему. Но еще раз повторюсь:
Объявляйте переменные по месту их использования.
Избегайте использования глобальных переменных. Если все же без них не обходиться, то не забывайте, что ваша библиотека может быть использована в многопотоковом приложении.
Определение и использование классов
Любой модуль можно логически разделить на две части:
определения - в которой определяются переменные, функции, классы, их методы и т.д.
использования - создание экземпляров классов в секции инициализации модуля.
Правило:
При планировании библиотеки классов не совмещайте в одном модуле части определения и использования. Или другими словами - отделяйте определение класса от того, как он будет использован.
Пример
Модуль Forms содержит определения классов, вспомогательных функций и создает экземпляры глобальных переменных (Application, Screen и т.д.). Допустим, в вашем консольном приложении, не использующем графический интерфейс нужна какая-то константа из модуля Forms. Включив его в свой проект, вы получите за бесплатно довесок в несколько сотен килобайт абсолютно ненужного вам кода. В чем причина? Линковщик не может определить, какие виртуальные методы будут вызваны, так как теоретически все они могут быть вызваны косвенно. По этому достаточно одного "упоминания" класса, как весь код его виртуальных методов (а также виртуальных методов других классов, на которые он ссылается) будет влинкован в ваше приложение, тут же. Во избежание подобной проблемы модуль Forms надо было бы разделить на две части: в одной - только определения, а в другой - создания экземпляров, выше указанных, глобальных переменных.
Я столкнулся с описанной проблемой при написании серверного приложения без GUI, которое взаимодействует с базой данных. Где-то в недрах DBxxx компонент есть ссылка на модуль Forms. Эта "особенность" была замечена в Delphi 5, скорее всего эта проблема имела место и в предыдущих версиях. Справедливости ради надо отметить, что в Delphi 7 эта особенность устранена.
Передача параметров
В Delphi параметры функций и процедур по умолчанию передаются по значению. Т.е. для них выделяется область памяти в стеке или куче, куда копируются оригинальные значения. При передаче параметров сложных типов (запись, массив, строка, вариант) это сопряжено со значительными расходами ресурсов, поэтому параметры этих типов желательно передавать по ссылке, т.е. с использованием ключевых слов var или const. Замечено, что наиболее типична эта ошибка при передаче параметра типа string.
Пример
procedure Proc(s : string); //Не очень хорошо :( procedure Proc(const s : string); //Гораздо лучше :) |
"Просачивание" исключений в библиотеках
При написании библиотеки функций или классов не закрывайте просачивание исключений наружу, если это конечно не предусмотрено логикой библиотеки.
Пример
try ... {Я столкнулся с подобным кодом в библиотеке ODBCExpress в процессе написания NT сервиса J.}
exception //Очень нехорошо
on Exception do ShowMessage('Something wrong's happened :-('); end; |
Обработка исключений, возникших в библиотеке - это задача приложения, которое использует данную библиотеку.
Range Check и Integer Overflow Check
К сожалению, эти опции компилятора по умолчанию отключены в Delphi, и многие разработчики не пользуются их услугами, а зря. Появления этих ошибок говорит о наличии в программе семантических ошибок, таких как неправильная индексация массива или использование несоответствующего целочисленного типа. Последствия этих ошибок могут быть весьма коварны. Я советую оставлять эти флаги всегда включенными, независимо от того - это отладочная или "финальная" версия программы. Лучше иметь неработающую программу (или ее часть), чем программу работающую неправильно (IMHO).
Отключать их имеет смысл, когда нет возможности исправить эту ошибку, как, например, в случае с ранними версиями VCL, скомпилированной с этими опциями.
Вступление
В течение своей профессиональной деятельности программист вырабатывает систему правил, которая позволяет ему не совершать допущенных ранее ошибок и избегать потенциально опасных ситуаций. Ценность правил заключается в том, что они ограждают программиста от не всегда очевидных проблем, дают возможность писать единообразный код и дают возможность поступать формально, тем самым, освобождая "мыслительные" ресурсы на решение поставленной задачи.
Некоторые из рецептов моей кулинарной книги я хочу предложить на ваше суждение. Очевидность этих правил зависит от вашей квалификации. Согласие с ними зависит от вашей собственной системы. Их источником послужили мой личный опыт и опыт ошибок начинающих программистов, каждое поколение которых повторяет их, к сожалению, с завидной стабильностью .
Выработанные правила направлены на:
Повышение надежности работы программы, т.е. уменьшение вероятности возникновения ошибки. Вероятность возникновения ошибки существует всегда, никто не безгрешен (включая операционную систему). Задача программиста - свести эту вероятность к минимуму.
Увеличение устойчивости программы - свойства, при котором она возвращается в стабильное состояние после возникновения возмущения (ошибки) (а не зависает, исчезает или уваливает операционную систему).
Написание единообразного и легко поддерживаемого кода.
Warnings and Hints
Компилятор Delphi снабжен "анализатором" качества кода. Он может предупреждать о потенциально опасных или бессмысленных ситуациях. Не пренебрегайте его услугами.
Правило:
Добивайтесь, что бы ваша программа компилировалась без предупреждений и намеков. Даже если они не существенны, в последствии в их массе вы или пользователи вашей библиотеки могут не заметить более важные предупреждения.
в этой статье носят общий
Правила приведенные в этой статье носят общий характер. Практически всегда существуют исключения (такова природа правил J). Следование этим правилам, позволило мне добиться разработки устойчивого и единообразного кода. Буду признательным за любые дополнения, исправления, замечания, примечания, пожелания и критику (особенно конструктивную).
С уважением,
февраль 2003г.
Специально для
Значения по умолчанию и "неопределенные" значения
В логике распределения значений для переменных всегда необходимо предусматривать "неопределенное" значение и значение по умолчанию. Отсутствие таких значений достаточно часто приводят к семантическим ошибкам.
Правило №1:
Для указателей и объектов пустым значением должно являться значение nil.
Для числовых типов лучше всего резервировать значение ноль.
Для строковых переменных - пустая строка
Для перечислимых типов необходимо предусмотреть специальное значение.
Пример:
TDayOfWeek = (dwNone,dwSun,dwMon,dwTue,dwWen,dwThu,dwFri,dwSat);
Правило №2:
"Неопределенными" значениями лучше всего выбирать такие, чье двоичное представление соответствует нулю (нулям). Это увеличивает устойчивость, когда не выполнена начальная инициализация переменной, но произведена инициализация блока памяти, в котором она размещается.
Пример
Для перечислимых типов "неопределенное" значение должно быть первым, так как оно соответствует целочисленному нулю.