Шаблоны в Object Pascal

Наверное каждый Delphi программист хоть раз общался с программистом C++ и объяснял насколько Delphi мощнее и удобнее. Но в некоторый момент, программист C++ заявляет примерно следующее "OK, но Delphi использует Pascal, а значит не поддерживает множественное наследование и шаблоны, поэтому он не так хорош как C++."

Насчёт множественного наследования можно легко заявить, что Delphi имеет интерфейсы, которые прекрасно справляются со своей задачей, но вот насчёт шаблонов Вам прийдётся согласится, так как Object Pascal не поддерживает их.

Давайте посмотрим на эту проблему по-внимательней Шаблоны позволяют делать универсальные контейнеры такие как списки, стеки, очереди, и т.д. Если Вы хотите осуществить что-то подобное в Delphi, то у Вас есть два пути:

Использовать контейнер TList, который содержит указатели. В этом случае Вам прийдётся всё время делать явное приведение типов. Сделать подкласс контейнера TCollection или TObjectList, и убрать все методы, зависящие от типов каждый раз, когда Вы захотите использовать новый тип данных. Третий вариант, это сделать модуль с универсальным классом контейнера, и каждый раз, когда нужно использовать новый тип данных, нам прийдётся в редакторе искать и вносить исправления. Было бы здорово, если всю эту работу за Вас делал компилятор.... вот этим мы сейчас и займёмся!

Например, возьмём классы TCollection и TCollectionItem. Когда Вы объявляете нового потомка TCollectionItem , то так же Вы наследуете новый класс от TOwnedCollection и переопределяете большинство методов, чтобы их можно было вызывать с новыми типами.

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

Шаг 1: Создайте новый текстовый файл (не юнитовский) с именем TemplateCollectionInterface.pas:


_COLLECTION_ = class (TOwnedCollection)  
protected  
 function  GetItem (const aIndex : Integer) : _COLLECTION_ITEM_;  
 procedure SetItem (const aIndex : Integer;  
                    const aValue : _COLLECTION_ITEM_);  
public  
 constructor Create (const aOwner : TComponent);  
  
 function Add                                 : _COLLECTION_ITEM_;  
 function FindItemID (const aID    : Integer) : _COLLECTION_ITEM_;  
 function Insert     (const aIndex : Integer) : _COLLECTION_ITEM_;  
 property Items      [const aIndex : Integer] : _COLLECTION_ITEM_ read GetItem write SetItem;  
end;  

Обратите внимание, что нет никаких uses или interface clauses, только универсальное объявление типа, в котором _COLLECTION_ это имя универсальной коллекции класса, а _COLLECTION_ITEM_ это имя методов, содержащихся в нашем шаблоне.

Шаг 2: Создайте второй текстовый файл и сохраните его как TemplateCollectionImplementation.pas:


constructor _COLLECTION_.Create (const aOwner : TComponent);  
begin  
 inherited Create (aOwner, _COLLECTION_ITEM_);  
end;  
  
function _COLLECTION_.Add : _COLLECTION_ITEM_;  
begin  
 Result := _COLLECTION_ITEM_ (inherited Add);  
end;  
  
function _COLLECTION_.FindItemID (const aID : Integer) : _COLLECTION_ITEM_;  
begin  
 Result := _COLLECTION_ITEM_ (inherited FindItemID (aID));  
end;  
  
function _COLLECTION_.GetItem (const aIndex : Integer) : _COLLECTION_ITEM_;  
begin  
 Result := _COLLECTION_ITEM_ (inherited GetItem (aIndex));  
end;  
  
function _COLLECTION_.Insert (const aIndex : Integer) : _COLLECTION_ITEM_;  
begin  
 Result := _COLLECTION_ITEM_ (inherited Insert (aIndex));  
end;  
  
procedure _COLLECTION_.SetItem (const aIndex : Integer;  
                                const aValue : _COLLECTION_ITEM_);  
begin  
 inherited SetItem (aIndex, aValue);  
end;   

Снова нет никаких uses или interface clauses , а только код универсального типа.

Шаг 3: Создайте новый unit-файл с именем MyCollectionUnit.pas:


unit MyCollectionUnit;  
  
interface  
  
uses Classes;  
  
type TMyCollectionItem = class (TCollectionItem)  
     private  
      FMyStringData  : String;  
      FMyIntegerData : Integer;  
     public  
      procedure Assign (aSource : TPersistent); override;  
     published  
      property MyStringData  : String  read FMyStringData  write FMyStringData;  
      property MyIntegerData : Integer read FMyIntegerData write FMyIntegerData;  
     end;  
  
     // !!! Указываем универсальному классу на реальный тип  
       
     _COLLECTION_ITEM_ = TMyCollectionItem;   
       
     // !!! директива добавления интерфейса универсального класса  
  
     {$INCLUDE TemplateCollectionInterface}   
  
     // !!! переименовываем универсальный класс  
  
     TMyCollection = _COLLECTION_;            
  
implementation  
  
uses SysUtils;  
  
// !!! препроцессорная директива добавления универсального класса  
  
{$INCLUDE TemplateCollectionImplementation}   
  
procedure TMyCollectionItem.Assign (aSource : TPersistent);  
begin  
 if aSource is TMyCollectionItem then  
 begin  
  FMyStringData  := TMyCollectionItem(aSource).FMyStringData;  
  FMyIntegerData := TMyCollectionItem(aSource).FMyIntegerData;  
 end  
 else inherited;  
end;  
  
end.  

Вот и всё! Теперь компилятор будет делать всю работу за Вас! Если Вы измените интерфейс универсального класса, то изменения автоматически распространятся на все модули, которые он использует.

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

Шаг 1: Создайте текстовый файл с именем TemplateVectorInterface.pas:


_VECTOR_INTERFACE_ = nterface  
 function  GetLength : Integer;  
 procedure SetLength (const aLength : Integer);  
  
 function  GetItems (const aIndex : Integer) : _VECTOR_DATA_TYPE_;  
 procedure SetItems (const aIndex : Integer;  
                     const aValue : _VECTOR_DATA_TYPE_);  
  
 function  GetFirst : _VECTOR_DATA_TYPE_;  
 procedure SetFirst (const aValue : _VECTOR_DATA_TYPE_);  
  
 function  GetLast  : _VECTOR_DATA_TYPE_;  
 procedure SetLast  (const aValue : _VECTOR_DATA_TYPE_);  
  
 function  High  : Integer;  
 function  Low   : Integer;  
  
 function  Clear                              : _VECTOR_INTERFACE_;  
 function  Extend   (const aDelta : Word = 1) : _VECTOR_INTERFACE_;  
 function  Contract (const aDelta : Word = 1) : _VECTOR_INTERFACE_;   
  
 property  Length                         : Integer             read GetLength write SetLength;  
 property  Items [const aIndex : Integer] : _VECTOR_DATA_TYPE_  read GetItems  write SetItems; default;  
 property  First                          : _VECTOR_DATA_TYPE_  read GetFirst  write SetFirst;  
 property  Last                           : _VECTOR_DATA_TYPE_  read GetLast   write SetLast;  
end;  
  
_VECTOR_CLASS_ = class (TInterfacedObject, _VECTOR_INTERFACE_)  
private  
 FArray : array of _VECTOR_DATA_TYPE_;  
protected  
 function  GetLength : Integer;  
 procedure SetLength (const aLength : Integer);  
  
 function  GetItems (const aIndex : Integer) : _VECTOR_DATA_TYPE_;  
 procedure SetItems (const aIndex : Integer;  
                     const aValue : _VECTOR_DATA_TYPE_);  
  
 function  GetFirst : _VECTOR_DATA_TYPE_;  
 procedure SetFirst (const aValue : _VECTOR_DATA_TYPE_);  
  
 function  GetLast  : _VECTOR_DATA_TYPE_;  
 procedure SetLast  (const aValue : _VECTOR_DATA_TYPE_);  
public  
 function  High  : Integer;  
 function  Low   : Integer;  
  
 function  Clear                              : _VECTOR_INTERFACE_;  
 function  Extend   (const aDelta : Word = 1) : _VECTOR_INTERFACE_;  
 function  Contract (const aDelta : Word = 1) : _VECTOR_INTERFACE_;   
  
 constructor Create (const aLength : Integer);  
end;  

Шаг 2: Создайте текстовый файл и сохраните его как TemplateVectorImplementation.pas:


constructor _VECTOR_CLASS_.Create (const aLength : Integer);  
begin  
 inherited Create;  
  
 SetLength (aLength);  
end;  
  
function _VECTOR_CLASS_.GetLength : Integer;  
begin  
 Result := System.Length (FArray);  
end;  
  
procedure _VECTOR_CLASS_.SetLength (const aLength : Integer);  
begin  
 System.SetLength (FArray, aLength);  
end;  
  
function _VECTOR_CLASS_.GetItems (const aIndex : Integer) : _VECTOR_DATA_TYPE_;  
begin  
 Result := FArray [aIndex];  
end;  
  
procedure _VECTOR_CLASS_.SetItems (const aIndex : Integer;  
                                   const aValue : _VECTOR_DATA_TYPE_);  
begin  
 FArray [aIndex] := aValue;  
end;  
  
function _VECTOR_CLASS_.High : Integer;  
begin  
 Result := System.High (FArray);  
end;  
  
function _VECTOR_CLASS_.Low : Integer;  
begin  
 Result := System.Low (FArray);  
end;  
  
function _VECTOR_CLASS_.GetFirst : _VECTOR_DATA_TYPE_;  
begin  
 Result := FArray [System.Low (FArray)];  
end;  
  
procedure _VECTOR_CLASS_.SetFirst (const aValue : _VECTOR_DATA_TYPE_);  
begin  
 FArray [System.Low (FArray)] := aValue;  
end;  
  
function _VECTOR_CLASS_.GetLast : _VECTOR_DATA_TYPE_;  
begin  
 Result := FArray [System.High (FArray)];  
end;  
  
procedure _VECTOR_CLASS_.SetLast (const aValue : _VECTOR_DATA_TYPE_);  
begin  
 FArray [System.High (FArray)] := aValue;  
end;  
  
function _VECTOR_CLASS_.Clear : _VECTOR_INTERFACE_;  
begin  
 FArray := Nil;  
  
 Result := Self;  
end;  
  
function _VECTOR_CLASS_.Extend (const aDelta : Word) : _VECTOR_INTERFACE_;  
begin  
 System.SetLength (FArray, System.Length (FArray) + aDelta);  
  
 Result := Self;  
end;  
  
function _VECTOR_CLASS_.Contract (const aDelta : Word) : _VECTOR_INTERFACE_;  
begin  
 System.SetLength (FArray, System.Length (FArray) - aDelta);  
  
 Result := Self;  
end;  

Шаг 3: Создайте unit файл с именем FloatVectorUnit.pas:

unit FloatVectorUnit;  
  
interface  
  
uses Classes;                           // !!! Модуль "Classes" содержит объявление класса TInterfacedObject  
  
type _VECTOR_DATA_TYPE_ = Double;       // !!! тип данных для класса массива Double  
  
     {$INCLUDE TemplateVectorInterface}  
  
     IFloatVector = _VECTOR_INTERFACE_; // !!! give the interface a meanigful name  
     TFloatVector = _VECTOR_CLASS_;     // !!! give the class a meanigful name  
  
function CreateFloatVector (const aLength : Integer = 0) : IFloatVector; // !!! дополнительная функция   
  
implementation  
  
{$INCLUDE TemplateVectorImplementation}  
  
function CreateFloatVector (const aLength : Integer = 0) : IFloatVector;       
begin  
 Result := TFloatVector.Create (aLength);  
end;  
  
end.   

Естевственно, можно дополнить универсальный класс дополнительными функциями. Всё зависит от Вашей фантазии!

Использование шаблонов Вот пример использования нового векторного интерфейса:


procedure TestFloatVector;  
 var aFloatVector : IFloatVector;  
     aIndex       : Integer;  
begin  
 aFloatVector := CreateFloatVector;  
  
 aFloatVector.Extend.Last := 1;  
 aFloatVector.Extend.Last := 2;  
  
 for aIndex := aFloatVector.Low to aFloatVector.High do  
 begin  
  WriteLn (FloatToStr (aFloatVector [aIndex]));  
 end;  
end.   

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

Добавлено: 07 Августа 2018 08:22:42 Добавил: Андрей Ковальчук

Регистрация в реестре

Несомненно, регистр (он же реестр) Windows — очень полезная вещь. Ведь в нем можно хранить так много информации — от количества программ вашей фирмы, ранее установленных на данном PC, и информации об их регистрации до разнообразных настроек окошек этих самых программ. Но для того чтобы все это там хранить, необходимо уметь работать с реестром, причем работать корректно, без ошибок.В данной статье будут разобраны основные функции работы с регистром в Delphi (легко портируемые при необходимости на С/С++), а также рассмотрены примеры пользовательских функций для более общих действий с регистром (тоже легко переносимые на С/С++). Итак, приступим…

Рассмотрим на примере. Допустим, вы написали очень (или не слишком) навороченную софтину и теперь хотите, чтобы богатенькие юзеры ею не просто так пользовались, а за денежку. То бишь, скачал юзер вашу софтинку с официального сайта вашей же фирмы Глюкософт, запустил, а она ему: «Заплати-ка ты, дружок, сначала $99.99 фирме Глюкософт, если хочешь получить все мои возможности, получи там у них код на такой-то номер, а потом и пользуйся!» Для этого вам необходимо при первом запуске сгенерировать два значения — «Программа не зарегистрирована» и уникальный номер — и где-то их сохранить. Уникальный номер нужен затем, чтобы предотвратить попытки доморощенных хакеров продавать за $9 универсальный код к вашей софтинке.

Первое и самое главное, что вам понадобится, это какая-либо переменная типа HKey. Этот тип описан в модуле windows.pas (как и все функции, описанные ниже) и представляет собой тип дескриптора ключа регистра (кто не знает, дескриптор — это типа указателя, только круче :-)).

Затем при помощи функции RegOpenKey или RegOpenKeyEx открыть необходимый нам ключ регистра. Если при этом возникнет ошибка, значит, на 99% можно утверждать, что софтина запускается впервые, или что ваши записи в регистре были удалены.

Синтаксис у нее такой:


Function RegOpenKey(BaseKey:HKey; SubKey:PChar; dwReserved:dword; samDesired:RegSAM; var ResKey:PHKey):dword;  

Где:

BaseKey — дескриптор ранее открытого ключа или одна из мнемонических констант — HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE или HKEY_USERS, обозначающих соответствующие базовые разделы регистра. Рекомендуется использовать только первые два: HKEY_CLASSES_ROOT — информация об обрабатываемых расширениях, и HKEY_CURRENT_USER — информация о текущем пользователе;

SubKey — имя ключа, который вы хотите создать. При этом оно может содержать \ для спуска на один уровень вниз, но не должно начинаться или заканчиваться на этот самый \;

dwReserved — припасено для будущих версий, должно быть 0;

samDesired — флаг или комбинация флагов, отвечающих за способ открытия ключа; возможны следующие значения: KEY_READ — просто чтение, KEY_SET_VALUE — установить значение переменной в ключе, KEY_WRITE — перезаписать ключ полностью, при этом все старые значения будут уничтожены;

ResKey — дескриптор-приемник ключа.

В случае успешного выполнения функция возвращает значение ERROR_SUCCESS или NO_ERROR, в случае ошибки возвращается код ошибки.

Пусть мы установили, что запуск произведен впервые (открытие ключа вернуло ошибку). Значит, теперь нужно создать соответствующий вашей программе ключ. Сделать это можно при помощи функции RegCreateKeyEx.

Синтаксис у нее следующий:


Function RegCreateKeyEx(BaseKey:HKey; SubKey:PChar; dwReserved:dword; pClass:PChar; dwOptions:dword; samDesired:RegSAM; SecAttr:lpSecurity_Attributes; var ResKey:PHKey; Disposition:lpdword):dword;
Большинство параметров аналогичны RegOpenKeyEx:

samDesired — может быть также KEY_ALL_ACCESS — полный доступ с предварительным созданием; KEY_CREATE_SUB_KEY — создание вложенного ключа; pClass — совершенно бесполезный параметр, поэтому лучше ставить значение nil; dwOptions — определяет тип создаваемого ключа; может быть REG_OPTION_NON_VOLATILE (постоянный ключ, используется по умолчанию; при этом информация, записанная в ключ, сохраняется и доступна после перезагрузки) или REG_OPTION_VOLATILE (временный ключ; в Win95/98 вообще игнорируется, во всех WinNT информация записывается во временную память и теряется при перезагрузке); SecAttr — в Win95/98 вообще игнорируется, во всех WinNT отвечает за параметры политики безопасности. Лучше ставить значение nil;

Disposition — еще один совершенно ненужный параметр; лучше использовать адрес любой переменной типа integer или word со значением 0.

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


Function RegSetValueEx(BaseKey:HKey; ValueName:PChar; dwReserved, dwType:dword; pData:pointer; DataSize:dword):dword;  

Где:

ValueName — имя переменной, значение которой устанавливается (если ее нет, то она будет создана);
dwType — идентификатор типа переменной: REG_BINARY — все виды двоичных типов переменных (напремер, boolean); REG_DWORD — переменная типа dword; REG_SZ — строковая переменная; REG_EXPAND_SZ — строка с мнемоническими обозначениями переменных среды (например, %PATH%\GluckoSoft); REG_NONE — неопределенный или сложный тип (например, какая-нибудь структура);
pData — указатель на записываемые данные;
DataSize — размер записываемых данных.

После всех этих операций нужно закрыть дескриптор ключа и освободить связанные с ним ресурсы системы. Делается это при помощи функции RegCloseKey с самым простым синтаксисом:


Function RegCloseKey(WhatKey:HKey):dword. 

Теперь рассмотрим пользовательскую функцию, которая все это будет делать:


…  
const {нам понадобятся некоторые константы}  
SKey = ‘Software\GluckoSoft\Someware’; {ключ для данной программы}  
SID = ‘ProductID’; {имя переменной, хранящей уникальный номер программы}  
SRg = ‘Registered’;{имя переменной, хранящей сведения о регистрации}  
…  
function Prepare:dword;  
var  
Res:dword;  
Key:HKey;  
Rgst:boolean;  
ID:dword;  
Dummy:integer;  
begin  
Prepare:=ERROR_SUCCESS;  
Dummy:=0;  
Rgst:=false;  
ID:=Random(10000);  
Res:=RegOpenKeyEx(HKEY_CURRENT_USER, SKey, 0, KEY_READ, Key);  
if (Res<>ERROR_SUCCESS)  
then  
begin  
Res:=RegCreateKey(HKEY_CURRENT_USER, SKey, 0, nil, REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, nil, Key, @Dummy);  
if (Res<>ERROR_SUCCESS)  
then  
begin  
Prepare:=Res;  
Exit;  
end  
else  
begin  
Res:=RegSetValueEx(Key,SRg,0,REG_BINARY,@Rgst,SizeOf(Rgst));  
if (Res<>ERROR_SUCCESS)  
then  
begin  
Prepare:=Res;  
Exit;  
end;  
Res:=RegSetValueEx(Key,SID,0,REG_DWORD,@ID,SizeOf(ID));  
if (Res<>ERROR_SUCCESS)  
then  
begin  
Prepare:=Res;  
Exit;  
end;  
RegCloseKey(Key);  
end;  
end  
else RegCloseKey(Key);  
end;  
…  

Значит, данные вы сохранили, окошко с сообщением юзеру выдали (наверняка сами это сможете сделать), тот зарегистрировался и получил код. Теперь необходимо код этот проверить на соответствие уникальному номеру и при соответствии отметить, что софтинка ваша зарегистрирована. Для этого вам понадобится уже знакомая функция RegOpenKeyEx с типом доступа KEY_READ и функция чтения RegQueryValueEx:


Function RegQueryValueEx(BaseKey:HKey; ValueName:PChar; pdwReserved,pdwType:lpdword; pData:pointer; pDataSize:lpdword):dword;  

Почти все параметры вам уже знакомы.

pdwReserved — зарезервировано, nil;
pdwType — указатель на приемник идентификатора типа указанной переменной; заполняется по выполнении функции;
pData — указатель на структуру-приемник данных;
pDataSize — указатель (!) на размер приемника данных.

С их помощью вы считываете значение уникального номера, проверяете его на соответствие введенному коду и, при удовлетворительном результате, функцией RegSetValueEx изменяете значение переменной Registered на true.

Реализовать это можно примерно так:


…  
function Register(Code:dword):dword;  
var  
Key:HKey;  
Res,D1,D2,ID:dword;  
Rgst:boolean;  
begin  
Register:=ERROR_SUCCESS;  
Res:=RegOpenKeyEx(HKEY_CURRENT_USER, SKey, 0, KEY_READ, Key);  
if (Res<>ERROR_SUCCESS)  
then  
begin  
Register:=Res;  
Exit;  
end  
else  
begin  
D2:=SizeOf(ID);  
Res:=RegQueryValue(Key, SID, nil, @D1, @ID, @D2);  
if (Res<>ERROR_SUCCESS)  
then  
begin  
Register:=Res;  
Exit;  
end;  
{проверяем соответствие кода уникальному номеру например так: уникальный номер равен коду, нацело деленному на два}  
if ((Code div 2)<>ID) {при несоответствии…}  
then  
begin  
Register:=666; {…возвращаем ошибку…}  
Exit; {…и выходим!}  
end;  
RegCloseKey(Key);  
Res:=RegOpenKeyEx(HKEY_CURRENT_USER, SKey, 0, KEY_SET_VALUE, Key);  
if (Res<>ERROR_SUCCESS)  
then  
begin  
Register:=Res;  
Exit;  
end;  
Rgst:=true;  
Res:=RegSetValueEx(Key, SRg, 0, REG_BINARY, @Rgst, SizeOf(Rgst));  
if (Res<>ERROR_SUCCESS)  
then  
begin  
Register:=Res;  
Exit;  
end;  
RegCloseKey(Key);  
end;  
end;  
…  

Осталось совсем чуть-чуть. Предположим, что юзер вашей несомненно великолепной софтиной попользовался, но по какой-то причине решил ее удалить (мало ли, может, лучше нашел… причем, тоже вашу :-)). Удалять при этом придется порядком всякой всячины, и конечно же, вы для этого создали специальную процедуру/программу. Нужно обязательно не забыть удалить еще и записи в реестре (желательно, только от своей программы :-)). В этом вам поможет функция RegDeleteKey: Function RegDeleteKey(BaseKey:HKey; SubKey:PChar):dword.

Тут все понятно. Выглядеть это будет приблизительно вот так:


…  
procedure UnInstall;  
…  
begin  
…  
RegDeleteKey(HKEY_CURRENT_USER, SKey);  
…  
end;  
…  

Нужно заметить, что при этом удаляется только подключ HKEY_CURRENT_USER\Software\GluckoSoft\Someware, а ключ HKEY_CURRENT_USER\Software\GluckoSoft остается. Чтобы избежать удаления информации других программ в этом ключе и, с другой стороны, захламления регистра пустыми ключами, можно, например, непосредственно в ключ HKEY_CURRENT_USER\Software\GluckoSoft добавить переменную, которая будет увеличиваться на 1 при установке новых программ вашей фирмы и уменьшаться на 1 при их удалении. Когда она станет равна нулю, смело производите удаление ключа. Заодно получится счетчик «рейтинга» вашей фирмы.

P.S. Вообще-то существует еще модуль registry.pas с описанием типа TRegistry, вроде бы призванного облегчить вам жизнь, но на практике гораздо удобнее и эффективнее использовать описанные выше функции. К тому же подключение этого модуля увеличивает размер ехе-файла.

Автор: Дмитрий НАЗАРАТИЙ

Добавлено: 07 Августа 2018 08:20:56 Добавил: Андрей Ковальчук

Работа с TDBGrid

Что можно поместить в DBGrid

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

Как изменить цвет строки в TDBGrid
Предположим, нам требуется изменить атрибуты текста и фона строки в компоненте TDBGrid, если значение какого-либо поля удовлетворяет заранее заданному условию. Для этой цели принято использовать обработчик события OnDrawColumnCell этого компонента. Отметим, что возможности, предоставляемые при его использовании, весьма разнообразны.

Рассмотрим простейшее приложение с TDBGrid, содержащее один компонент TTable, один компонент TDataSource и один компонент TDBGrid: Установим значения их свойств в соответствии с приведенной ниже таблицей:

Компонент Свойство Значение
Table1 DatabaseName BCDEMOS (или DBDEMOS)
TableName events.db
Active true
DataSource1 DataSet Table1
DBGrid1 DataSource DataSource1
Обычно для перерисовки изображения в ячейках используется метод OnDrawColumnCell.

Его параметр Rect - структура, описывающая занимаемый ячейкой прямоугольник, параметр Column - колонка DBGrid, в которой следует изменить способ рисования изображения. Для вывода текста используется метод TextOut свойства Canvas компонента TDBGrid.

Предположим, нам нужно изменить цвет текста и фона строки в зависимости от значения какого-либо поля (например, VenueNo). Создадим обработчик события OnDrawColumnCell компонента DBGrid1. В случае C++Builder он имеет вид:


void __fastcall TForm1::DBGrid1DrawColumnCell(TObject *Sender,  
      const TRect &Rect, int DataCol, TColumn *Column,  
      TGridDrawState State)  
{  
if (Table1->FieldByName("VenueNo")->Value==1)  
{  
DBGrid1->Canvas->Brush->Color=clGreen;  
DBGrid1->Canvas->Font->Color=clWhite;  
DBGrid1->Canvas->FillRect(Rect);  
DBGrid1->Canvas->TextOut(Rect.Left+2,Rect.Top+2,Column->Field->Text);  
}  
}  

В случае Delphi соответствующий код имеет вид:


procedure TForm1.DBGrid1DrawColumnCell(Sender: TObject; const Rect: TRect;  
  DataCol: Integer; Column: TColumn; State: TGridDrawState);  
begin  
if (Table1.FieldByName('VenueNo').Value=1) then begin  
with  DBGrid1.Canvas do begin  
Brush.Color:=clGreen;  
Font.Color:=clWhite;  
FillRect(Rect);  
TextOut(Rect.Left+2,Rect.Top+2,Column.Field.Text);  
end;  
end;  
end;  

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

При выводе выделенных строк все данные в ячейках оказались выровненными по левому краю. Если мы хотим более корректно отобразить выравнивание текста в колонке, следует слегка модифицировать наш код, учтя значение свойства Alignment текущей (то есть рисуемой в данный момент) колонки:


void __fastcall TForm1::DBGrid1DrawColumnCell(TObject *Sender,  
      const TRect &Rect, int DataCol, TColumn *Column,  
      TGridDrawState State)  
{  
if (Table1->FieldByName("VenueNo")->Value==1)  
{  
DBGrid1->Canvas->Brush->Color=clGreen;  
DBGrid1->Canvas->Font->Color=clWhite;  
DBGrid1->Canvas->FillRect(Rect);  
if (Column->Alignment==taRightJustify)  
{  
DBGrid1->Canvas->TextOut(Rect.Right-2-  
  DBGrid1->Canvas->TextWidth(Column->Field->Text),  
  Rect.Top+2,Column->Field->Text);  
}  
else  
{  
DBGrid1->Canvas->TextOut(Rect.Left+2,Rect.Top+2,Column->Field->Text);  
}  
}  
}  

Соответствующий код для Delphi имеет вид:


procedure TForm1.DBGrid1DrawColumnCell(Sender: TObject; const Rect: TRect;  
  DataCol: Integer; Column: TColumn; State: TGridDrawState);  
begin  
if (Table1.FieldByName('VenueNo').Value=1) then begin  
with  DBGrid1.Canvas do begin  
Brush.Color:=clGreen;  
Font.Color:=clWhite;  
FillRect(Rect);  
if (Column.Alignment=taRightJustify) then  
 TextOut(Rect.Right-2-  TextWidth(Column.Field.Text),  
  Rect.Top+2,Column.Field.Text)  
else  
 TextOut(Rect.Left+2,Rect.Top+2,Column.Field.Text);  
end;  
end;  
end;  

В этом случае выравнивание текста в колонках совпадает с выравниванием столбцов.

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

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


void __fastcall TForm1::DBGrid1DrawColumnCell(TObject *Sender,  
      const TRect &Rect, int DataCol, TColumn *Column,  
      TGridDrawState State)  
{  
if ((Table1->FieldByName("VenueNo")->Value==1) &  
(Column->FieldName=="VenueNo") )  
{  
DBGrid1->Canvas->Brush->Color=clGreen;  
DBGrid1->Canvas->Font->Color=clWhite;  
DBGrid1->Canvas->FillRect(Rect);  
DBGrid1->Canvas->TextOut(Rect.Right-2-  
  DBGrid1->Canvas->TextWidth(Column->Field->Text),  
  Rect.Top+2,Column->Field->Text);  
}  
}  

Соответствующий код для Delphi имеет вид:


procedure TForm1.DBGrid1DrawColumnCell(Sender: TObject; const Rect: TRect;  
  DataCol: Integer; Column: TColumn; State: TGridDrawState);  
begin  
if (Table1.FieldByName('VenueNo').Value=1) and  
(Column.FieldName='VenueNo')  then begin  
with  DBGrid1.Canvas do begin  
Brush.Color:=clGreen;  
Font.Color:=clWhite;  
FillRect(Rect);  
TextOut(Rect.Right-2-  TextWidth(Column.Field.Text),  
  Rect.Top+2,Column.Field.Text)  
end;  
end;  
end;  

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

Как заменить данные в столбце компонента TDBGrid
Нередко в колонке DBGrid нужно вывести не реальное значение, хранящееся в поле соответствующей таблицы, а другие данные, соответствующие имеющимся (например, символьную строку вместо ее числового кода). В этом случае также используется метод TextOut свойства Canvas компонента TDBGrid:


void __fastcall TForm1::DBGrid1DrawColumnCell(TObject *Sender,  
      const TRect &Rect, int DataCol, TColumn *Column,  
      TGridDrawState State)  
{  
if  
(Column->FieldName=="VenueNo")  
{  
DBGrid1->Canvas->Brush->Color=clWhite;  
DBGrid1->Canvas->FillRect(Rect);  
if (Table1->FieldByName("VenueNo")->Value==1)  
{  
DBGrid1->Canvas->Font->Color=clRed;  
DBGrid1->Canvas->TextOut(Rect.Right-2-  
  DBGrid1->Canvas->TextWidth("our venue"),  
  Rect.Top+2,"our venue");  
}  
else  
{  
DBGrid1->Canvas->TextOut(Rect.Right-2-  
  DBGrid1->Canvas->TextWidth("other venue"),  
  Rect.Top+2,"other venue");  
}  
}  
}  

Соответствующий код для Delphi имеет вид:


procedure TForm1.DBGrid1DrawColumnCell(Sender: TObject; const Rect: TRect;  
  DataCol: Integer; Column: TColumn; State: TGridDrawState);  
begin  
if  (Column.FieldName='VenueNo')  then begin  
with  DBGrid1.Canvas do begin  
Brush.Color:=clWhite;  
FillRect(Rect);  
if (Table1.FieldByName('VenueNo').Value=1) then begin  
Font.Color:=clRed;  
TextOut(Rect.Right-2-  
  DBGrid1.Canvas.TextWidth('our venue'),  
  Rect.Top+2,'our venue');  
end else begin  
 TextOut(Rect.Right-2-  
  DBGrid1.Canvas.TextWidth('other venue'),  
  Rect.Top+2,'other venue');  
end;  
end;  
end;  
end;  

Еще один пример - использование значков из шрифтов Windings или Webdings в качестве подставляемой строки.


void __fastcall TForm1::DBGrid1DrawColumnCell(TObject *Sender,  
      const TRect &Rect, int DataCol, TColumn *Column,  
      TGridDrawState State)  
{  
if  
(Column->FieldName=="VenueNo")  
{  
DBGrid1->Canvas->Brush->Color=clWhite;  
DBGrid1->Canvas->FillRect(Rect);  
DBGrid1->Canvas->Font->Name="Wingdings";  
DBGrid1->Canvas->Font->Size=-14;  
if (Table1->FieldByName("VenueNo")->Value==1)  
{  
DBGrid1->Canvas->Font->Color=clRed;  
DBGrid1->Canvas->TextOut(Rect.Right-2-  
  DBGrid1->Canvas->TextWidth("J"),  
  Rect.Top+1,"J");  
}  
else  
{  
DBGrid1->Canvas->Font->Color=clBlack;  
DBGrid1->Canvas->TextOut(Rect.Right-2-  
  DBGrid1->Canvas->TextWidth("F"),  
  Rect.Top+1,"F")    ;  
}  
}  
}  

Соответствующий код для Delphi имеет вид:


procedure TForm1.DBGrid1DrawColumnCell(Sender: TObject; const Rect: TRect;  
  DataCol: Integer; Column: TColumn; State: TGridDrawState);  
begin  
if  (Column.FieldName='VenueNo')  then begin  
with  DBGrid1.Canvas do begin  
Brush.Color:=clWhite;  
FillRect(Rect);  
Font.Name:='Wingdings';  
Font.Size:=-14;  
if (Table1.FieldByName('VenueNo').Value=1) then begin  
Font.Color:=clRed;  
TextOut(Rect.Right-2-  
  DBGrid1.Canvas.TextWidth('J'),  
  Rect.Top+1,'J');  
end else begin  
 Font.Color:=clBlack;  
 TextOut(Rect.Right-2- DBGrid1.Canvas.TextWidth('F'),  
  Rect.Top+1,'F');  
end;  
end;  
end;  
end;  

Как поместить графическое изображение в TDBGrid
Использование свойства Canvas компонента TDBGrid в методе OnDrawColumnCell позволяет не только выводить в ячейке текст методом TextOut, но и размещать в ячейках графические изображения. В этом случае используется метод Draw свойства Canvas.

Модифицируем наш пример, добавив на форму компонент TImageList и поместив в него несколько изображений.

Модифицируем код нашего приложения:


void __fastcall TForm1::DBGrid1DrawColumnCell(TObject *Sender,  
      const TRect &Rect, int DataCol, TColumn *Column,  
      TGridDrawState State)  
{  
 Graphics::TBitmap *Im1;  
 Im1= new   Graphics::TBitmap;  
if  
(Column->FieldName=="VenueNo")  
{  
DBGrid1->Canvas->Brush->Color=clWhite;  
DBGrid1->Canvas->FillRect(Rect);  
if (Table1->FieldByName("VenueNo")->Value==1)  
{  
ImageList1->GetBitmap(0,Im1);  
}  
else  
{  
ImageList1->GetBitmap(2,Im1);  
}  
DBGrid1->Canvas->Draw((Rect.Left+Rect.Right-Im1->Width)/2,Rect.Top,Im1);  
}  
}  

Соответствующий код для Delphi имеет вид:


procedure TForm1.DBGrid1DrawColumnCell(Sender: TObject; const Rect: TRect;  
  DataCol: Integer; Column: TColumn; State: TGridDrawState);  
var Im1: TBitmap;  
begin  
Im1:=TBitmap.Create;  
if  (Column.FieldName='VenueNo' ) then begin  
with  DBGrid1.Canvas do begin  
Brush.Color:=clWhite;  
FillRect(Rect);  
if (Table1.FieldByName('VenueNo').Value=1)  
then begin  
ImageList1.GetBitmap(0,Im1);  
end else begin  
ImageList1.GetBitmap(2,Im1);  
end;  
Draw(round((Rect.Left+Rect.Right-Im1.Width)/2),Rect.Top,Im1);  
end;  
end;  
end;  

Теперь в TDBGrid в колонке VenueNo находятся графические изображения.

Добавлено: 07 Августа 2018 08:18:46 Добавил: Андрей Ковальчук

Создание Анимации в Delphi

В этом примере показано, как, объеденив классы Delphi 5 с функциями Win32 GDI, можно добиться анимации упрощенного избражения эльфа.

Исходные тексты можно взять здесь.


unit MainFrm;  
interface  
uses  
SysUtils, WinTypes, WinProcs, Messages, Classes,  
Graphics, Controls, Forms, Dialogs, Menus, Stdctrls;  
{$R SPRITES.RES } {Привязка растровых изображений к 
исполняемому файлу.}  
type  
TSprite = class  
private  
FWidth: integer;  
FHeight: integer;  
FLeft: integer;  
FTop: integer;  
FAndImage, FOrImage: TBitMap;  
public  
property Top: Integer read FTop write FTop;  
property Left: Integer read FLeft write FLeft;  
property Width: Integer read FWidth write FWidth;  
property Height: Integer read FHeight write FHeight;  
constructor Create;  
destructor Destroy; override;  
end;  
  
TMainForm = class(TForm)  
procedure FormCreate(Sender: TObject);  
procedure FormPaint(Sender: TObject);  
procedure FormDestroy(Sender: TObject);  
private  
BackGnd1, BackGnd2: TBitMap;  
Sprite: TSprite;  
GoLeft, GoRight,GoUp,GoDown: boolean;  
procedure MyIdleEvent(Sender: TObject;  
var Done: Boolean);  
procedure DrawSprite;  
end;  
  
const  
BackGround = 'BACK2.BMP';  
var  
  
MainForm: TMainForm;  
  
implementation  
  
{$R *.DFM}  
constructor TSprite.Create;  
begin  
inherited Create;  
{ Создание растров для хранения изображений эльфа, 
которые будут использованы при выполнении операции 
AND/OR (И/ИЛИ) для содания анимации }  
FAndImage := TBitMap.Create;  
FAndImage.LoadFromResourceName(hInstance, 'AND');  
  
FOrImage := TBitMap.Create;  
FOrImage.LoadFromResourceName(hInstance, 'OR');  
  
Left := 0;  
Top := 0;  
Height := FAndImage.Height;  
Width := FAndImage.Width;  
end;  
  
destructor TSprite.Destroy;  
begin  
FAndImage.Free;  
FOrImage.Free;  
inherited Destroy;  
end;  
  
  
procedure TMainForm.FormCreate(Sender: TObject);  
begin  
// Создание исходного фонового изображения  
BackGnd1 := TBitMap.Create;  
with BackGnd1 do  
begin  
LoadFromResourceName(hInstance, 'BACK');  
Parent := nil;  
SetBounds(0, 0, Width, Height);  
end;  
  
// Создание копии фонового изображения  
BackGnd2 := TBitMap.Create;  
BackGnd2.Assign(BackGnd1);  
  
// Создание изображения эльфа  
Sprite := TSprite.Create;  
  
// Инициализация переменных направления  
GoRight := true;  
GoDown := true;  
GoLeft := false;  
GoUp := false;  
  
{ Установка события приложения OnIdle равным значению 
MyIdleEvent, с которого начнется движение эльфа }  
Application.OnIdle := MyIdleEvent;  
// Установка высоты и ширины области клиента формы  
ClientWidth := BackGnd1.Width;  
ClientHeight := BackGnd1.Height;  
end;  
  
procedure TMainForm.FormDestroy(Sender: TObject);  
begin  
// Освобождение всех объектов, созданных в конструкторе  
формы FormCreate()  
BackGnd1.Free;  
BackGnd2.Free;  
Sprite.Free;  
end;  
  
procedure TMainForm.MyIdleEvent(Sender: TObject;  
var Done: Boolean);  
begin  
DrawSprite;  
{ Разрешение вызова события OnIdle даже при отсутствии 
сообщений в очереди сообщений приложения }  
Done := False;  
end;  
  
procedure TMainForm.DrawSprite;  
var  
OldBounds: TRect;  
begin  
  
// Сохранение границ эльфа в объекте OldBounds  
with OldBounds do  
begin  
Left := Sprite.Left;  
Top := Sprite.Top;  
Right := Sprite.Width;  
Bottom := Sprite.Height;  
end;  
  
{ Теперь изменяем границы эльфа, чтобы он двигался в одном 
направлении, или изменяем направление при 
соприкосновении с границами формы }  
with Sprite do  
begin  
if GoLeft then  
if Left > 0 then  
Left := Left - 1  
else begin  
GoLeft := false;  
GoRight := true;  
end;  
  
if GoDown then  
if (Top + Height) < self.ClientHeight then  
Top := Top + 1  
else begin  
GoDown := false;  
GoUp := true;  
end;  
  
if GoUp then  
if Top > 0 then  
Top := Top - 1  
else begin  
GoUp := false;  
GoDown := true;  
end;  
  
if GoRight then  
if (Left + Width) < self.ClientWidth then  
Left := Left + 1  
else begin  
GoRight := false;  
GoLeft := true;  
end;  
end;  
{ Стираем исходное изображение эльфа на фоне BackGnd2 
путем копирования прямоугольника из фона BackGnd1 }  
with OldBounds do  
BitBlt(BackGnd2.Canvas.Handle, Left, Top, Right, Bottom,  
BackGnd1.Canvas.Handle, Left, Top, SrcCopy);  
{ Теперь рисуем эльфа на "внеэкранном" растре, тем самым 
избавлясь от мерцания }  
with Sprite do  
begin  
{ Создадим черное пятно с силуэтом эльфа с помощью 
операции логического И, выполненной над растрами 
FAndImage и BackGnd2 }  
BitBlt(BackGnd2.Canvas.Handle, Left, Top, Width, Height,  
FAndImage.Canvas.Handle, 0, 0, SrcAnd);  
// Выполним заливку черного пятна исходными цветами эльфа  
BitBlt(BackGnd2.Canvas.Handle, Left, Top, Width, Height,  
FOrImage.Canvas.Handle, 0, 0, SrcPaint);  
end;  
{ Копируем эльфа в его новой позиции на канву формы. 
При этом используется прямоугольник, который немного 
больше, чем нужно для фигуры эльфа. Тем самым мы 
добиваемся эффективного стирания эльфа путем его 
перезаписи, после чего рисуем нового эльфа в новой 
позиции с помощью доного вызова функции BitBlt }  
with OldBounds do  
BitBlt(Canvas.Handle, Left-2, Top-2, Right+2, Bottom+2,  
BackGnd2.Canvas.Handle, Left-2, Top-2, SrcCopy);  
end;  
procedure TMainForm.FormPaint(Sender: TObject);  
begin  
// Рисуем фоновой изображение при закрашивании формы  
BitBlt(Canvas.Handle, 0, 0, ClientWidth, ClientHeight,  
BackGnd1.Canvas.Handle, 0, 0, SrcCopy);  
end;  
  
end.  

Как это работает. Анимационный проект состоит из фонового изображения и нарисованного на нем эльфа в виде летающего блюдца, которое перемещается в пределах области клиента фона. Фон представлен растровым изображением разбросанных по небу звезд (рис 1).

Эльф составлен из двух растров размером 64x32. О них речь пойдет ниже, а пока рассмотрим, что происходит в программе. В приведенном модуле определяется класс TSprite, который содержит поля, предназначенные для хранения позиций эльфа на изображении фона, и два объекта типа TBitmap для хранения растровых изображений эльфа. Конструктор TSprite.Create создает оба экземпляра класса TBitmap и загружает их реальными растрами. Оба растровых изображения эльфа и фоновый растр содержатся в файле ресурсов, который привязывается к проекту путем включения в основной модуль следующей инструкции: { $R SPRITES.RES }.

После загрузки растра устанавливаются границы изображения эльфа. Деструктор TSprite.Destroy освобождает оба экземпляра растра. Главная форма содержит два объекта типа TBitmap, объект TSprite и индикаторы напрвлений, задающие линию движения эльфа. Кроме того, в главной форме определены два метода: MyIdleEvent(), служащий обработчиком событий Application.OnIdle, и DrawSprite(), предназначенный для рисования изображения эльфа.

Обработчик событий FormCreate() создает оба экземпляра класса TBitmap и загружает каждый одним и тем же растровым изображением (зачем - разберемся чуть ниже). Затем создается экземпляр класса TSprite, устанавливаются значения индикаторов направлений и обработчику событий Application.OnIdle назначается метод MyIdleEvent(). Наконец, обработчик событий формы FormCreate() изменяет размеры формы в соответствии с размерами фонового изображения.

Метод FormPaint() выполняет рисование на канве фона BackGnd1.

Метод FormDestroy() освобождает экземпляры классов TBitmap и TSprite.

Метод MyIdleEvent() вызывает метод DrawSprite(), который перемещает и рисует эльфа на существующем фоне.

Метод MyIdleEvent() вызывается, когда приложение находится в состоянии ожидания, т.е. когда пользователь не выполняет никаких действий, на которые приложению следовало бы отреагировать.

Метод DrawSprite() изменяет расположение эльфа на изображении фона. Для этого требуется выполнить немало инструкций - ведь сначала нужно стереть старое изображение эльфа, а затем нарисовать его на новом месте, сохраняя цвет фона вокруг реального изображения эльфа. Кроме того, метод DrawSprite() должен выполнить эти действия без мерцания. Для достижения поставленных целей процесс рисования выполняется на "внеэкранном" растре BackGnd2. Растры BackGnd2 и BackGnd1 являются точными копиями фонового изображения, однако BackGnd1 никогда не модифицируется (поэтому его можно назвать чистой копией фона). По завершении рисования модифицированная область растра BackGnd2 копируется на канву формы. Это позволяет за одно обращение к функции BitBlt() выполнить как стирание на канве формы, так и рисование эльфа в новой позиции. Какие же опреции выполняются с растром BackGnd2?

Во-первых, из BackGnd1 в BackGnd2 копируется прямоугольный участок, превышающий по размерам область, занимаемую самим эльфом. Тем самым гарантируется стирание изображения эльфа с растра BackGnd2. После этого растр FAndImage копируется в BackGnd2 на его новой позиции с помощью поразрядной опреции AND (логическое И). Это приводит к созданию черного пятна с силуэтом эльфа, но с сохранением цветов в области растра BackGnd2, окружающей черный силуэт. Растр FAndImage показан на (рис 2).

На рис 2 эльф представлен черными пикселами, а изображение вокруг эльфа состоит из белых пикселей. Черный цвет имеет значение, равное 0, а белый - 1. В табл. 1 и 2 приведены результаты выполнения опреции AND с белым и черным цветами.

Табл. 1 "Опреция AND с черным цветом"Фон Значение Цвет

BackGnd2 1001 Некоторый цвет
FAndImage 0000 Черный
Результат 1001 Черный
Табл. 2 "Операция AND с белым цветом"Фон Значение Цвет

BackGnd2 1001 Некоторый цвет
FAndImage 1111 Белый
Результат 1001 Некоторый цвет
Эти таблицы показывают, как выполнение операции логического И приводит к зачернению области, занимаемой эльфом на растре BackGnd2. В табл. 1 столбец "Значение" представляет цвет пискселя. Если пиксель на растре BackGnd2 содержит некоторый произвольный цвет, то объединение этого цвета с черным при использовании оператора AND заставит этот пиксель полностью почернеть. Аналогичная операция, выполненная над тем же цветом и абсолютно белым "коллегой" никак не отразится наисходном цвете, как видно в табл. 2. А поскольку цвет фона, на котором находится эльф в растре FAndImage, был белым, то пиксели на растре BackGnd2 копируются без изменения своих цветов. После копирования растра FAndImage в объект BackGnd2 растр FOrImage должен быть скопирован в то же самое место растра BackGnd2, чтобы заполнить черное пятно, созданное объединением растра FAndImage с реальными цветами эльфа. Растр FOrImage таже имеет прямоугольник, окружающий реальное изображение эльфа. И вновь мы сталкиваемся с задачей получения цветов эльфа для растра BackGnd2 и одновременным сохранением цветов этого растра в области, окружающей эльфа. Это достигается объединением растров FOrImage и BackGnd2 с использованием оператора OR (лигическое ИЛИ).

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

Табл. 3 "Операция OR с черным цветом"Фон Значение Цвет

BackGnd2 1001 Некоторый цвет
FOrImage 0000 Черный
Результат 1001 Некоторый цвет
Напомним, что все рисование выполняется на "внеэкранном" растре. По завершении рисования достаточно только одного обращения к функции BitBlt(), чтобы стереть и скопировать изображение эльфа. В описанном способе создания анимации нет ничего необычного. Вы можете сами расширить функциональные возможности класса, связанные с переме.

Добавлено: 07 Августа 2018 08:16:36 Добавил: Андрей Ковальчук

Создание собственных компонентов

Тебе предстоит познакомится с технологией создания собственных компонентов. Я расскажу, как можно создать компонент Delphi (не путай с компонентами ActiveX). В качестве примера я выбрал достаточно простой, но напичканый математикой пример - часы. Наши часы смогут работать как аналоговые и числовые. По этим часам будет Москва сверятся :).

Компоненты - это самая удобная вещь. С самого появления языков программирования, все программеры стремятся добится многократности использования кода. Для этого мы перешли к процедурному программирования, затем к объектному и сейчас нам предлагают перейти к компонентному программированию. То, что предлагает нам MS - использовать компоненты ActiveX я назову настоящей лажей. Эти компоненты требую гемороя для регистрации их на машине клиента, слежка за версиями и многое другое. Borland предложила свои компоненты. По умолчанию они встраиваются прямо в запускной файл и не требуют никакой регистрации и великолепно работают.

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

Для создания нового компонента выбери меню Component-> New Component.

Давай расмотрим каждое окошечко в отдельности:

Ancestor type - Тип предка. Это имя объекта, от которого мы порадим наш объект. Это даст нашему объекту все возможности его предка, плюс мы добавим свои. Для часиков нам понадобится TGraphicControl.
Class Name - Имя нашего будущего компонента. Я его назвал TGraphicClock.
Palette Page - Имя палитры компонентов, куда будет помещён наш компонент после инсталляции. Я оставил значение по умолчанию "Samples". Но ты можешь поместить его даже на закладку "Standart".
Unit file name - имя и путь к модулю, где будет располагатся исходный код компонента. Хотябы посмотри под каким именем сохранят твой компонент и где.
Search path - Сюда можно даже не заглядывать.
Жмём "ОК" (именно "ОК", а не Install) и получаем созданный Дульфой код:


unit GraphicClock;  
  
interface  
  
uses  
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs;  
  
type  
  TGraphicClock = class(TGraphicControl)  
  private  
    { Private declarations }  
  protected  
    { Protected declarations }  
  public  
    { Public declarations }  
  published  
    { Published declarations }  
  end;  
  
procedure Register;  
  
implementation  
  
procedure Register;  
begin  
  RegisterComponents('Samples', [TGraphicClock]);  
end;  
  
end.  

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

Начнём написание нашего компонента с конструктора и деструктора. конструктор - это простая процедура, которая автоматически вызывается при создании компонента. Деструктор - тоже процедура, только она автоматически вызывается при уничтожении компонента. В конструкторе мы проинициализируем все наши переменные, а в деструкторе уничтожим.

Итак, напиши в разделе public:


constructor Create(AOwner: TComponent); override;  

Теперь нажми сочетание клавишь CTRL+SHIFT+C. Дельфи сам создаст заготовку для конструктора:


constructor TGraphicClock.Create(AOwner: TComponent);  
begin  
  inherited;  
end;  

Поправим её до вот такого вида:


constructor TCDClock.Create(AOwner: TComponent);  
begin  
//Вызываем конструктор предка  
 inherited Create(AOwner);  
  
//Устанавливаем значения ширины и высоты по умолчанию  
 Width := 50;  
 Height := 50;  
  
//Устанавливаем переменную ShowSecondArrow в true.  
//Она будет у нас отвечать за показ секундной стрелки  
 ShowSecondArrow := true;  
  
//Инициализируем остальные переменные  
 PrevTime := 0;  
 CHourDiff := 0;  
 CMinDiff := 0;  
  
//Инициализируем растры TBitmap, в которых будут хранится  
//фон и сам рисунок часов.  
 FBGBitmap:= TBitmap.Create;  
 FFont:=TFont.Create;;  
  
 FBitmap:= TBitmap.Create;  
 FBitmap.Width := Width;  
 FBitmap.Height := Height;  
  
 //Выставляем формат времени  
 DateFormat:='tt';  
  
 //Запускаем таймер  
 Ticker := TTimer.Create( Self);  
 //Интервал работы таймера - одна секунда  
 Ticker.Interval := 1000;  
 //По событию OnTimer будет вызыватсья процедура TickerCall  
 Ticker.OnTimer := TickerCall;  
 //Включаем таймер  
 Ticker.Enabled := true;  
  
//Устанавливаем цвета поумолчанию  
 FFaceColor := clBtnFace;  
 FHourArrowColor := clActiveCaption;  
 FMinArrowColor := clActiveCaption;  
 FSecArrowColor := clActiveCaption;  
end;  

Ключевое слово inherited вызывает конструктор предка (в нашем случае TGraphicClock). В остальном, я надеюсь, что с конструктором всё ясно.

Теперь создадим деструктор. Для этого также опишем его в разделе public:


public  
  { Public declarations }  
  constructor Create(AOwner: TComponent); override;  
  destructor Destroy; override;  

Кстати, ключевое слово override; после имени этих процедур говорит о том, что мы хотим переписать уже существующую у предка функцию с таким именем. Теперь жмём Ctrl+Shift+C и получаем заготовку для деструктора и поправляем её до вида :


destructor TGraphicClock.Destroy;  
begin  
 Ticker.Free;  
 FBitmap.Free;  
 FBGBitmap.Free;  
 inherited Destroy;  
end;  

Заметь, что в конструкторе я вызывал предка в самом начале inherited, а в деструкторе в самом конце. В конструкторе сначала нужно, чтобы проинициализировался предок (он проинициализирует необходимые ссылки), а потом можно инициализировать свои вещи. Если в деструкторе мы сначала вызовем предка, то последующая работа с компонентом уже будет невозможна, потому что предок уничтожит все ссылки, поэтому я ставлю этот вызов в конце.

Теперь опишем все необходимые нам переменные в разделе private:


private  
  //Для часов обязательно понадобится таймер  
  Ticker: TTimer;  
  
  //Картинки часов и фона       
  FBitmap, FBGBitmap: TBitmap;  
  
  /События  
  FOnSecond, FOnMinute, FOnHour: TNotifyEvent;  
  
  //Центральная точка  
  CenterPoint: TPoint;  
  //Радиус  
  Radius: integer;  
  
  //Остальные параметры, которые мы рассмотрим в процессе.  
  LapStepW: integer;  
  PrevTime: TDateTime;  
  ShowSecondArrow: boolean;  
  FHourArrowColor,FMinArrowColor, FSecArrowColor: TColor;  
  FFaceColor: TColor;  
  CHourDiff, CMinDiff: integer;  
  FClockStyle:TClockStyle;  
  FDateFormat: String;  
  FFont: TFont;  

Теперь в разделе private опицем процедуру TickerCall. Мы её уже использовали в конструкторе. Она у нас вызывается по событию от таймера:


protected  
  { Protected declarations }  
  procedure TickerCall(Sender: TObject);  

Жмём CTRL+SHIAT+C и модифицируем созданную функцию:


procedure TCDClock.TickerCall(Sender: TObject);  
var  
 H,M,S,Hp,Mp,Sp: word;  
begin  
 //Если компонент создан в дезайнере, то выход  
 if csDesigning in ComponentState then exit;  
 //Иначе это уже запущеная программа  
  
 //Получить время  
 DecodeCTime( Time, H, M, S);  
 //Получить предыдущее время.  
 DecodeCTime( PrevTime, Hp, Mp, Sp);  
  
 //Сгенерировать событие OnSecond  
 if Assigned( FOnSecond) then FOnSecond(Self);  
 //Сгенерировать событие  OnMinute  
 if Assigned( FOnMinute) AND (Mp < M) then FOnMinute(Self);  
 //Сгенерировать событие  OnHour  
 if Assigned( FOnHour) AND (Hp < H) then FOnHour(Self);  
  
 //Сохранить текущее время в PrevTime  
 PrevTime := Time;  
  
 if ( NOT ShowSecondArrow) AND (Sp <= S) then exit;  
  
 //Прорисовать часы.  
 DrawArrows;  
end;  

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

У нас уже объявлено три события:


FOnSecond, FOnMinute, FOnHour: TNotifyEvent;  

Все они появятся на закладке Events в окне Object Inspector, когда ты поставишь компонент на форму. Чтобы приложения могло поймать эти события, мы объявили переменные типа TNotifyEvent и генерируем эти события (с помощью конструкции типа FOnSecond(Self) для события OnSecond), когда изменилась секунда, изменилась минута или изменился час.

Помимо этого, в разделе published мыдолжны описать свойство OnSecond:


property OnSecond: TNotifyEvent read FOnSecond write FOnSecond;  
property OnMinute: TNotifyEvent read FOnMinute write FOnMinute;  
property OnHour: TNotifyEvent read FOnHour write FOnHour;  

Теперь наш компонент сможет генерировать события.

С событиями вроде всё ясно (если нет, то посмотри на исходник в конце статьи). Теперь переходим к свойствам. В разделе published мы можем создавать свойства, которые будут отображатся в Object Inspector при выделении наших часиков. Все свойства, которые есть у предка нужно просто описать


published  
  property Align;  
  property Enabled;  
  property ParentShowHint;  
  property ShowHint;  
  property Visible;  

Слово property говорит о том, что мы описываем свойство. Для них не нужны процедуры или функции, потому что эти свойства уже есть у предка. Нам надо только описать их и всё. Я описал только маленькую часть из доступных у TGraphicControl функций. Ты можешь добавить любые из доступных. Чтобы узнать, какие функции можно добавлять, открой помощь (меню Help->Delphi Help) и найди там объект TGraphicControl. Щёлкни по нему дважды и в появившейся справке выбери пункт Properties (вверху окна) (рис 2). Появится окно с перечнем всех свойств. Ты можешь добавить любое из них. Например, чтобы добавить свойство Action нужно написать в разделе published:


published  
  property Action;  

Чтобы добавить своё свойство, нужно немного попатеть. Например. Добавим возможность, чтобы пользователь мог менять картинку фона. Для этого описываем в разделе published свойство BGBitmap:


//свойство Имя     :Тип     читать из FBGBitmap записывать с помощью SetBGBitmap  
  property BGBitmap:TBitmap read FBGBitmap      write SetBGBitmap;  

Коментарий поможет тебе разобраться в написанном здесь. Итак, мы объявили свойство BGBitmap типа ТBitmap. Для чтения используется простая переменная FBGBitmap (можно использовать и функцию, но нет смысла, потому что можно прямо читать из переменной), для записи используется процедура SetBGBitmap. Процедура выглядит так:


procedure TCDClock.SetBGBitmap(Value: TBitmap);  
begin  
 FBGBitmap.Assign(Value);  
 invalidate;  
end;  

Теперь ты можешь изменять фон простой операцией GraphicClock1.BGBitmap:=bitmap.

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


//свойство Имя       :Тип         Это нам известно                 Значение по умолчанию  
  property ClockStyle:TClockStyle read FClockStyle write SetStyleStyle default scAnalog;  

Мы объявляем свойство ClockStyle типа TClockStyle. Тип TClockStyle мы должны описать в самом начале, до описания нашего объекта TGraphicClock:


type  
  TClockStyle = (scAnalog, scDigital);  
  
  TGraphicClock = class(TGraphicControl)  
  private  
    Ticker: TTimer;  
Строка TClockStyle = (scAnalog, scDigital) - объявляет список переменных, которые и будут выпадать по выбору свойства.

Всё остально происходит так же, за исключением нового слова default Которое устанавливает значение по умолчанию - scAnalog.

Вот и всё, что я хотел тебе рассказать.

Чтобы установить компонент в системе нужно щёлкнуть меню Component->Install Component. Перед тобой появится окно.

В строке Unit file name нужно указать полный путь к файлу. Для облегчения выбора используй кнопку Browse

справа. Перед тобой появится запрос на компиляцию пакета. Соглашайся.

В этом окне ты можешь откомпелировать пакет с помощью кнопки Compile и установить в Delphi с помощью Install .

Вот и всё. Наслаждайся новым компонентом в твоей палитре.

На сегодня хватит. Статья и так получилась достаточно большая. Увидимся в следующий раз.

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

Добавлено: 07 Августа 2018 08:15:23 Добавил: Андрей Ковальчук

Создание хранителя экрана (ScreenSaver)

Главное о чем стоит упомянуть это, что ваш хранитель экрана будет работать в фоновом режиме и он не должен мешать работе других запущенных программ. Поэтому сам хранитель должен быть как можно меньшего объема. Для уменьшения объема файла в описанной ниже программе не используется визуальные компоненты Delphi, включение хотя бы одного из них приведет к увеличению размера файла свыше 200кб, а так, описанная ниже программа, имеет размер всего 20Кб!

Технически, хранитель экрана является нормальным EXE файлом (с расширением .SCR), который управляется через командные параметры строки. Например, если пользователь хочет изменить параметры вашего хранителя, Windows выполняет его с параметром "-c" в командной строке. Поэтому начать создание вашего хранителя экрана следует с создания примерно следующей функции:


procedure RunScreenSaver;  
var S : String;  
begin  
  S := ParamStr(1);  
  if (Length(S) > 1) then begin  
    Delete(S,1,1); { delete first char - usally "/" or "-" }  
    S[1] := UpCase(S[1]);  
  end;  
  LoadSettings; { load settings from registry }  
  if (S = 'C') then RunSettings  
  else If (S = 'P') then RunPreview  
  else If (S = 'A') then RunSetPassword  
  else RunFullScreen;  
end;  

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

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


procedure RunFullScreen;  
var  
  R : TRect;  
  Msg : TMsg;  
  Dummy : Integer;  
  Foreground : hWnd;  
begin  
  IsPreview := False; MoveCounter := 3;   
  Foreground := GetForegroundWindow;  
  while (ShowCursor(False) > 0) do ;  
  GetWindowRect(GetDesktopWindow,R);  
  CreateScreenSaverWindow(R.Right-R.Left,R.Bottom-R.Top,0);  
  CreateThread(nil,0,@PreviewThreadProc,nil,0,Dummy);  
  SystemParametersInfo(spi_ScreenSaverRunning,1,@Dummy,0);  
  while GetMessage(Msg,0,0,0) do  
    begin  
    TranslateMessage(Msg);  
    DispatchMessage(Msg);  
  end;  
  SystemParametersInfo(spi_ScreenSaverRunning,0,@Dummy,0);  
  ShowCursor(True);  
  SetForegroundWindow(Foreground);  
end;  

Во-первых, мы проинициализировали некоторые глобальные переменные (описанные далее), затем прячем курсор мыши и создаем окно хранителя экрана. Имейте в виду, что важно уведомлять Windows, что это - хранителя экрана через SystemParametersInfo (это выводит из строя Ctrl-Alt-Del чтобы нельзя было вернуться в Windows не введя пароль). Создание окна хранителя:


function CreateScreenSaverWindow(Width,Height : Integer; ParentWindow : hWnd) : hWnd;  
var WC : TWndClass;  
begin  
  with WC do  
    begin  
    Style := cs_ParentDC;  
    lpfnWndProc := @PreviewWndProc;  
    cbClsExtra := 0; cbWndExtra := 0; hIcon := 0; hCursor := 0;  
    hbrBackground := 0; lpszMenuName := nil;   
    lpszClassName := 'MyDelphiScreenSaverClass';  
    hInstance := System.hInstance;  
  end;  
  RegisterClass(WC);  
  if (ParentWindow 0) then  
    Result := CreateWindow('MyDelphiScreenSaverClass','MySaver'ws_Child Or ws_Visible or  
ws_Disabled,0,0,Width,Height,ParentWindow,0,hInstance,nil)  
  else  
    begin  
    Result := CreateWindow('MyDelphiScreenSaverClass','MySaver',ws_Visible or ws_Popup,0,0,Width,Height,  
0,0,hInstance,nil);SetWindowPos(Result,hwnd_TopMost,0,0,0,0,swp_NoMove or swp_NoSize or swp_NoRedraw);  
  end;  
  PreviewWindow := Result;  
end;  

Теперь окна созданы используя вызовы API. Я удалил проверку ошибки, но обычно все проходит хорошо, особенно в этом типе приложения.

Теперь Вы можете погадать, как мы получим handle родительского окна предварительного просмотра? В действительности, это совсем просто: Windows просто передает handle в командной строке, когда это нужно. Таким образом:


procedure RunPreview;  
var  
  R : TRect;  
  PreviewWindow : hWnd;  
  Msg : TMsg;  
  Dummy : Integer;  
begin  
  IsPreview := True;  
  PreviewWindow := StrToInt(ParamStr(2));  
  GetWindowRect(PreviewWindow,R);  
  CreateScreenSaverWindow(R.Right-R.Left,R.Bottom-R.Top,PreviewWindow);  
  CreateThread(nil,0,@PreviewThreadProc,nil,0,Dummy);  
  while GetMessage(Msg,0,0,0) do  
    begin  
    TranslateMessage(Msg);  
        DispatchMessage(Msg);  
  end;  
end;  

Как Вы видите, window handle является вторым параметром (после "-p").

Чтобы "выполнять" хранителя экрана - нам нужна нить. Это создается с вышеуказанным CreateThread. Процедура нити выглядит примерно так:


function PreviewThreadProc(Data : Integer) : Integer; StdCall;  
var R : TRect;  
begin  
  Result := 0; Randomize;  
  GetWindowRect(PreviewWindow,R);  
  MaxX := R.Right-R.Left; MaxY := R.Bottom-R.Top;  
  ShowWindow(PreviewWindow,sw_Show); UpdateWindow(PreviewWindow);  
  repeat  
    InvalidateRect(PreviewWindow,nil,False);  
    Sleep(30);  
  until QuitSaver;  
  PostMessage(PreviewWindow,wm_Destroy,0,0);  
end;  

Нить просто заставляет обновляться изображения в нашем окне, спит на некоторое время, и обновляет изображения снова. А Windows будет посылать сообщение WM_PAINT на наше окно (не в нить!). Для того, чтобы оперировать этим сообщением, нам нужна процедура:


function PreviewWndProc(Window : hWnd; Msg,WParam,LParam : Integer): Integer; StdCall;  
begin  
  Result := 0;  
  case Msg of  
    wm_NCCreate : Result := 1;  
    wm_Destroy : PostQuitMessage(0);  
    wm_Paint : DrawSingleBox; { paint something }  
    wm_KeyDown : QuitSaver := AskPassword;  
    wm_LButtonDown, wm_MButtonDown, wm_RButtonDown, wm_MouseMove :   
    begin  
      if (Not IsPreview) then  
            begin  
        Dec(MoveCounter);  
        if (MoveCounter <= 0) then QuitSaver := AskPassword;  
      end;  
    end;  
  else  
      Result := DefWindowProc(Window,Msg,WParam,LParam);  
  end;  
end;  

Если мышь перемещается, кнопка нажата, мы спрашиваем у пользователя пароль:


function AskPassword : Boolean;  
var  
  Key : hKey;  
  D1,D2 : Integer; { two dummies }  
  Value : Integer;  
  Lib : THandle;  
  F : TVSSPFunc;  
begin  
  Result := True;  
  if (RegOpenKeyEx(hKey_Current_User,'Control Panel\Desktop',0,Key_Read,Key) = Error_Success) then  
  begin  
    D2 := SizeOf(Value);  
    if (RegQueryValueEx(Key,'ScreenSaveUsePassword',nil,@D1,@Value,@D2) = Error_Success) then  
    begin  
      if (Value 0) then  
            begin  
        Lib := LoadLibrary('PASSWORD.CPL');  
        if (Lib > 32) then  
                begin  
          @F := GetProcAddress(Lib,'VerifyScreenSavePwd');  
          ShowCursor(True);  
          if (@F nil) then Result := F(PreviewWindow);  
          ShowCursor(False);  
          MoveCounter := 3; { reset again if password was wrong }  
          FreeLibrary(Lib);  
        end;  
      end;  
    end;  
  RegCloseKey(Key);  
  end;  
End;  

Это также демонстрирует использование Registry на уровне API. Также имейте в виду как мы динамически загружаем функции пароля, используюя LoadLibrary. Запомните тип функции? TVSSFunc ОПРЕДЕЛЕН как:


type  
  TVSSPFunc = Function(Parent : hWnd) : Bool; StdCall;  

Теперь почти все готово, кроме диалога конфигурации. Это запросто:


procedure RunSettings;  
var Result : Integer;  
begin  
  Result := DialogBox(hInstance,'SaverSettingsDlg',0,@SettingsDlgProc);  
  if (Result = idOK) then SaveSettings;  
end;  

Трудная часть -это создать диалоговый сценарий (запомните: мы не используем здесь Delphi-формы!). Я сделал это, используя 16-битовую Resource Workshop (остался еще от Turbo Pascal для Windows). Я сохранил файл как сценарий (текст), и скомпилированный это с BRCC32:


SaverSettingsDlg DIALOG 70, 130, 166, 75  
STYLE WS_POPUP | WS_DLGFRAME | WS_SYSMENU  
CAPTION "Settings for Boxes"  
FONT 8, "MS Sans Serif"  
BEGIN  
DEFPUSHBUTTON "OK", 5, 115, 6, 46, 16  
PUSHBUTTON "Cancel", 6, 115, 28, 46, 16  
CTEXT "Box &Color:", 3, 2, 30, 39, 9  
COMBOBOX 4, 4, 40, 104, 50, CBS_DROPDOWNLIST | CBS_HASSTRINGS  
CTEXT "Box &Type:", 1, 4, 3, 36, 9  
COMBOBOX 2, 5, 12, 103, 50, CBS_DROPDOWNLIST | CBS_HASSTRINGS  
LTEXT "Boxes Screen Saver for Win32 Copyright (c) 1996 Jani  
Jдrvinen.", 7, 4, 57, 103, 16,  
WS_CHILD | WS_VISIBLE | WS_GROUP  
END  

Почти также легко сделать диалоговое меню:


function SettingsDlgProc(Window : hWnd; Msg,WParam,LParam : Integer): Integer; stdcall;  
var S : String;  
begin  
  Result := 0;  
  case Msg of  
    wm_InitDialog :  
        begin  
      { initialize the dialog box }  
      Result := 0;  
    end;  
    wm_Command :  
        begin  
      if (LoWord(WParam) = 5) then EndDialog(Window,idOK)  
      else if (LoWord(WParam) = 6) then EndDialog(Window,idCancel);  
    end;  
    wm_Close : DestroyWindow(Window);  
    wm_Destroy : PostQuitMessage(0);  
  else  
      Result := 0;  
  end;  

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


procedure SaveSettings;  
var  
  Key : hKey;  
  Dummy : Integer;  
begin  
  if  
(RegCreateKeyEx(hKey_Current_User,'Software\SilverStream\SSBoxes',0,nil,  
  
Reg_Option_Non_Volatile,Key_All_Access,nil,Key,@Dummy)  
= Error_Success) then  
    begin  
    RegSetValueEx(Key,'RoundedRectangles',0,Reg_Binary,@RoundedRectangles,SizeOf(Boolean));  
    RegSetValueEx(Key,'SolidColors',0,Reg_Binary, @SolidColors,SizeOf(Boolean));  
    RegCloseKey(Key);  
  end;  
end;  

Загружаем параметры так:


procedure LoadSettings;  
var  
  Key : hKey;  
  D1,D2 : Integer; { two dummies }  
  Value : Boolean;  
begin  
  if (RegOpenKeyEx(hKey_Current_User,'Software\SilverStream\SSBoxes',0,Key_Read,Key) = Error_Success) then  
    begin  
    D2 := SizeOf(Value);  
    if (RegQueryValueEx(Key,'RoundedRectangles',nil,@D1,@Value, @D2) = Error_Success) then  
      RoundedRectangles := Value;  
    if (RegQueryValueEx(Key,'SolidColors',nil,@D1,@Value,@D2) = Error_Success) then  
      SolidColors := Value;  
    RegCloseKey(Key);  
  end;  
end;  

Легко? Нам также нужно позволить пользователю, установить пароль. Я честно не знаю почему это оставлено разработчику приложений! Тем не менее:


procedure RunSetPassword;  
var  
  Lib : THandle;  
  F : TPCPAFunc;  
begin  
  Lib := LoadLibrary('MPR.DLL');  
  if (Lib > 32) then  
    begin  
    @F := GetProcAddress(Lib,'PwdChangePasswordA');  
    if (@F nil) then F('SCRSAVE',StrToInt(ParamStr(2)),0,0);  
    FreeLibrary(Lib);  
  end;  
end;  

Мы динамически загружаем (недокументированную) библиотеку MPR.DLL, которая имеет функцию, чтобы установить пароль хранителя экрана, так что нам не нужно беспокоиться об этом. TPCPAFund определён как:


type  
  TPCPAFunc = Function(A : PChar; Parent : hWnd; B,C : Integer) : Integer; StdCall;  

Не спрашивайте меня что за параметры B и C ! :-)

Теперь единственная вещь, которую нам нужно рассмотреть - самая странная часть: создание графики. Я не великий гуру графики, так что вы не увидите затеняющие многоугольники, вращающиеся в реальном времени. Я только сделал некоторые ящики.


procedure DrawSingleBox;  
var  
  PaintDC : hDC;  
  Info : TPaintStruct;  
  OldBrush : hBrush;  
  X,Y : Integer;  
  Color : LongInt;  
begin  
  PaintDC := BeginPaint(PreviewWindow,Info);  
  X := Random(MaxX); Y := Random(MaxY);  
  if SolidColors then  
    Color := GetNearestColor(PaintDC,RGB(Random(255),Random(255),Random(255)))  
  else  
      Color := RGB(Random(255),Random(255),Random(255));  
  OldBrush := SelectObject(PaintDC,CreateSolidBrush(Color));  
  if RoundedRectangles then  
    RoundRect(PaintDC,X,Y,X+Random(MaxX-X),Y+Random(MaxY-Y),20,20)  
  else  
      Rectangle(PaintDC,X,Y,X+Random(MaxX-X),Y+Random(MaxY-Y));  
  DeleteObject(SelectObject(PaintDC,OldBrush));  
  EndPaint(PreviewWindow,Info);  
end;  

И последнее — глобальные переменные:


var  
  IsPreview : Boolean;  
  MoveCounter : Integer;  
  QuitSaver : Boolean;  
  PreviewWindow : hWnd;  
  MaxX,MaxY : Integer;  
  RoundedRectangles : Boolean;  
  SolidColors : Boolean;  

Затем исходная программа проекта (.dpr). Красива, а!?


program MySaverIsGreat;  
   
uses Windows, messages, Utility; { defines all routines }  
   
{$R SETTINGS.RES}  
   
begin  
  RunScreenSaver;   
end.  

Ох, чуть не забыл! Если, Вы используете SysUtils в вашем проекте (например фуекцию StrToInt) вы получите EXE-файл больше чем обещанный в 20K :-) Если Вы хотите все же иметь 20K, надо как-то обойтись без SysUtils, например самому написать собственную StrToInt процедуру.

Если все же очень трудно обойтись без использования Delphi-форм, то можно поступить как в случае с вводом пароля: форму изменения параметров хранителя сохранить в виде DLL и динамически ее загружать при необходимости. Т.о. будет маленький и шустрый файл самого хранителя экрана и довеска DLL для конфигурирования и прочего (там объем и скорость уже не критичны).

Добавлено: 07 Августа 2018 08:13:19 Добавил: Андрей Ковальчук

Способы сохранения и загрузки параметров программного обеспечения

В этой статье речь пойдет о способах сохранения и загрузки параметров программного обеспечения. Из своего личного опыта я могу твердо сказать, что это не так просто, как кажется многим. Как Вы уже успели заметить, крупные программные продукты используют для хранения своих параметров исключительно системный реестр. Напротив, разработчики программного обеспечения, относящие его к Freeware, предпочитают конфигурационные файлы с расширением “INI” (далее “ini-файлы”). Почему же дело обстоит именно так? Мы рассмотрим два этих способа более подробно, а так же поговорим о внедрении определенных средств защиты ini-файлов.

Системный реестр – один из самых надежных, но отнюдь не самый безопасный способов хранения параметров. Данные в нем располагаются в виде иерархической структуры, что облегчает поиск нужного раздела, но тем самым увеличивает риск удаления данных. Угрозой потери данных, находящихся в реестре, кроме неосторожных действий самих пользователей, могут являться сбои в операционной системе Microsoft Windows. Но это уже другая история.

Для работы с системным реестром в Borland Delphi предусмотрен модуль Registry, который содержит класс TRegistry.


Uses Registry;  
…  
…  
Var  
  R: TRegistry;  
Begin  
  R := Tregistry.Create;  
  …  
  …  
  …  
  R.Free;  
End;  

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


procedure GetCaption;  
var  
  R: TRegistry;  
begin  
  R := TRegistry.Create;  
  R.RootKey := HKEY_LOCAL_MACHINE;  
  { 
    Открытие ключа. Параметр True означает, 
    что при отсутствии ключа он автоматически создается 
  }  
  R.OpenKey('Software\Test', True)  
  {Записываем параметр}  
  R.WriteString('FormCaption', Form1.Caption);  
  {Закрываем ключ}  
  R.CloseKey;  
  R.Free;  
end;  
  
procedure SaveCaption;  
var  
  R: TRegistry;  
begin  
  R := TRegistry.Create;  
  R.RootKey := HKEY_LOCAL_MACHINE;  
  R.OpenKey('Software\Test', True)  
  { 
    Устанавливаем заголовок формы, используя 
    ранее сохраненную строчку 
  }  
  Form1.Caption := R.ReadString('FormCaption');  
  R.CloseKey;  
  R.Free;  
end;  

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

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

Стоит сказать еще о двух процедурах модуля Registry: GetKeyNames и GetValueNames. Они позволяют сканировать реестр, как обычные каталоги. При открытии ключа вы указываете начальный путь поиска, а далее процедура GetKeyNames создает список типа TStrings и записывает в него имена всех найденных ключей, а процедура GetValueNames составляет список из имен параметров, находящихся в заданном ключе.

До появления 32-х разрядных операционных систем, для хранения параметров программы использовали исключительно конфигурационные файлы с расширением “INI” (далее ini-файлы). Но вскоре, ini-файлы были забыты, и на смену им пришел системный реестр. Но до сих пор встречаются программы, которые активно используют такой способ хранения параметров. В чем же его преимущества? Прежде всего в стабильности. В отличие от системного реестра, при сбоях в операционной системе с ini-файлом ничего не случается, если только он не находится в системном каталоге. Стоит помнить, что ini-файл должен находиться в одном каталоге с программой. Кроме стабильности, важным преимуществом ini-файлов является мобильность программного кода, в чем вы можете убедиться, посмотрев пример, приведенный ниже. Что же касается недостатков, то это простота удаления. Ini-файл – это обычный файл, который можно случайно удалить.

Для работы с ini-файлами в Borland Delphi предусмотрен модуль IniFiles.


Uses IniFiles;  
…  
…  
…  
Var  
  IniFile: TiniFile;  
Begin  
  IniFile := TiniFile.Create(‘имя файла’);  
  …  
  …  
  …  
  IniFile.Free;  
End;  

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


procedure SaveCaption;  
var  
  IniFile: TIniFile;  
begin  
  IniFile := TIniFile.Create(ExtractFilePath(Application.Exename) + 'Test.ini');  
  IniFile.WriteString('MainOptions', 'FormCaption', Form1.Caption);  
  IniFile.Free;  
end;  
  
procedure GetCaption;  
var  
  IniFile: TIniFile;  
begin  
  IniFile := TIniFile.Create(ExtractFilePath(Application.Exename) + 'Test.ini');  
  Form1.Caption := IniFile.ReadString('MainOptions', 'FormCaption', '');  
  IniFile.Free;  
end;  

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

А теперь, поговорим о средствах защиты. Операционная система предоставляет возможность закрытия доступа к определенным файлам. Но, к сожалению, закрытие доступа длится только во время работы программы. После того, как программа будет закрыта, доступ будет полностью открыт. Например, можно ограничить доступ к ini-файлу. Для реализации данной возможности, среди множества функций WinAPI существует функция OpenFile. Давайте попробуем написать программу, которая закрывала бы доступ к ini-файлу.


 var  
  Form1: TForm1;  
  hIniLockedFile: Cardinal;  
  OfStruct      : _OfStruct;  
implementation  
 {$R *.dfm}  
procedure SaveCaption;  
var  
  IniFile: TIniFile;  
begin  
  {Отключаем защиту файла}  
  CloseHandle(hIniLockedFile);  
  {Резервное время для оключения}  
  Sleep(1000);  
  IniFile := TIniFile.Create(ExtractFilePath(Application.Exename) + 'Test.ini');  
  IniFile.WriteString('MainOptions', 'FormCaption', Form1.Caption);  
  IniFile.Free;  
  {После сохранения, заново ставим защиту}  
  hIniLockedFile := OpenFile(PChar(ExtractFileDir(Application.Exename) + 'Test.ini'), OfStruct, OF_Share_Exclusive);  
end;  
  
procedure GetCaption;  
var  
  IniFile: TIniFile;  
begin  
  IniFile := TIniFile.Create(ExtractFilePath(Application.Exename) + 'Test.ini');  
  Form1.Caption := IniFile.ReadString('MainOptions', 'FormCaption', '');  
  IniFile.Free;  
  {Устанавливаем защиту}  
  hIniLockedFile := OpenFile(PChar(ExtractFileDir(Application.Exename) + 'Test.ini'), OfStruct, OF_Share_Exclusive);  
end;  

При запуске программы, из ini-файла считывается информация, после чего он блокируется для последующих операций работы с ним: копирование, удаление, редактирование и т.д. При сохранении параметров, защита временно снимается, а вскоре заново возобновляется.

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

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

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

Я написал две простейшие процедуры кодирования и декодирования текста. На их примере мы и рассмотрим работу нашей программы.


 var  
  Form1: TForm1;  
  hIniLockedFile: Cardinal;  
  OfStruct      : _OfStruct;  
  
const  
  csCryptFirst = 20;  
  csCryptSecond = 230;  
  csCryptHeader = 'Crypted';  
  
type  
  ECryptError = class(Exception);  
  
implementation  
  
{$R *.dfm}  
  
function CryptString(Str:String):String;  
var  
  I    : Integer;  
  Clen : Integer;  
begin  
  clen := Length(csCryptHeader);  
  SetLength(Result, Length(Str)+clen);  
  Move(csCryptHeader[1], Result[1], clen);  
  For i := 1 to Length(Str) do  
   begin  
    if i mod 2 = 0 then  
     Result[i+clen] := Chr(Ord(Str[i]) xor csCryptFirst)  
    else  
     Result[i+clen] := Chr(Ord(Str[i]) xor csCryptSecond);  
   end;  
end;  
  
function UnCryptString(Str:String):String;  
var  
  I    : Integer;  
  Clen : Integer;  
begin  
  Clen := Length(csCryptHeader);  
  SetLength(Result, Length(Str)-Clen);  
  If Copy(Str, 1, clen) <> csCryptHeader then  
   raise ECryptError.Create('Файл поврежден!');  
  For i := 1 to Length(Str)-clen do  
   begin  
    if (i) mod 2 = 0 then  
     Result[i] := Chr(Ord(Str[i+clen]) xor csCryptFirst)  
    else  
     Result[i] := Chr(Ord(Str[i+clen]) xor csCryptSecond);  
   end;  
end;  
  
Procedure CryptIniFile;  
var  
  S: TStringList;  
  I: Integer;  
begin  
  S := TStringList.Create;  
  S.LoadFromFile(ExtractFileDir(Application.Exename) + 'Test.ini');  
  For I := 0 to S.Count - 1 do  
    S.Strings[I] := CryptString(S.Strings[I]);  
  S.SaveToFile(ExtractFileDir(Application.Exename) + 'Test.ini');  
end;  
  
Procedure DecryptIniFile;  
var  
  S: TStringList;  
  I: Integer;  
begin  
  if not FileExists(ExtractFileDir(Application.Exename) + 'Test.ini') then Exit;  
  S := TStringList.Create;  
  S.LoadFromFile(ExtractFileDir(Application.Exename) + 'Test.ini');  
  For I := 0 to S.Count - 1 do  
    S.Strings[I] := UnCryptString(S.Strings[I]);  
  S.SaveToFile(ExtractFileDir(Application.Exename) + 'Test.ini');  
end;  
  
procedure SaveCaption;  
var  
  IniFile: TIniFile;  
begin  
  {Отключаем защиту файла}  
  CloseHandle(hIniLockedFile);  
  {Резервное время для оключения}  
  Sleep(1000);  
  IniFile := TIniFile.Create(ExtractFilePath(Application.Exename) + 'Test.ini');  
  IniFile.WriteString('MainOptions', 'FormCaption', Form1.Caption);  
  IniFile.Free;  
  {После сохранения, заново ставим защиту}  
  hIniLockedFile := OpenFile(PChar(ExtractFileDir(Application.Exename) + 'Test.ini'), OfStruct, OF_Share_Exclusive);  
end;  
  
procedure GetCaption;  
var  
  IniFile: TIniFile;  
begin  
  {Расширофка конфигурационного файла}  
  DeCryptInIFile;  
  Sleep(1000);  
  IniFile := TIniFile.Create(ExtractFilePath(Application.Exename) + 'Test.ini');  
  Form1.Caption := IniFile.ReadString('MainOptions', 'FormCaption', '');  
  IniFile.Free;  
  {Устанавливаем защиту}  
  hIniLockedFile := OpenFile(PChar(ExtractFileDir(Application.Exename) + 'Test.ini'), OfStruct, OF_Share_Exclusive);  
end;  
  
procedure TForm1.FormCreate(Sender: TObject);  
begin  
  GetCaption;  
end;  
  
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);  
begin  
  SaveCaption;  
  CloseHandle(hIniLockedFile);  
  CryptInIFile;  
end;  


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

Следует сказать пару слов о самих алгоритмах кодирования и декодирования текста. В качестве признака того, что файл уже был закодирован, используется обыкновенная текстовая строка «Crypted», которой соответствует константа csCryptHeader. Она следует вначале каждой зашифрованной строчки. Перед шифрованием, определяется длина строки csCryptHeader, после чего увеличивается длина будущей строки, которая равна длине шифруемой строки и длине строки csCryptHeader. Далее, простым перебором, происходит замена каждого символа шифруемой строки. Это осуществляется с помощью функции Chr, которая по сгенерированному программой числу возвращает соответствующий символ. При замене символов, следует учитывать определенные параметры, которые определяет логическая операция. В нашем примере, в зависимости от того, делится ли порядковый номер строки на два без остатка, используются разные параметры, что усложняет последующую дешифровку текста.

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

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

Автор: Корнейчук Михаил

Добавлено: 07 Августа 2018 08:02:33 Добавил: Андрей Ковальчук

Простейший AI на примере мини-игры (часть 1)

Большинство компьютерных игр содержат содержат искусственный интеллект. Если брать во внимание все серьёзные игры (Action, RPG и т.п.), то они полностью "захвачены искусственным разумом". Другое дело - мини-игры. К примеру, общеизвестный Сапёр прекрасно живёт и без интеллекта, думать ему во время игры вообще не нужно... Да что говорить и самой игре, если в некоторых ситуациях мыслительный процесс самого игрока ни к чему не приведёт - бывают ситуации, когда нужно просто щёлкнуть наугад - тут уже вероятность 50% - либо попал на мину, либо не попал... :-)

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

Сейчас мы попытаемся создать самый простой AI (Artificial Intelligence кстати) на примере небольшой игры.

Игра

Давайте создадим игру, которую можно назвать "Догонялки". Задача игрока очень проста: управляя своим героем, стараться не попасть в лапы соперника. Очень простая задумка, но тем не менее здесь будет AI, хотя и простейший. Пусть программа автоматически увеличивает уровень сложности по мере игры.

Проектируем интерфейс

Сделаем игру на основе стандартных компонент. Не будем применять никакой графики, ибо речь совсем не об этом. Итак, пусть наши "герои" будут простыми... квадратами. А почему бы и нет? Нет у нас времени на рисование персонажей - всё будет абстрактно. Размещаем на форме 2 компонента TShape (вкладка Additional палитры компонент). Один сделаем красным, а другой синим. Цвет заливки задаётся свойством Brush - Color. Пусть наш герой будет синим, а враг - красным. С персонажами определились. Что нам ещё нужно? Наверное, кнопка для запуска игры. Поместите на форму кнопку, лучше в левый верхний угол. Ну и ещё разместим где-нибудь 3 текстовые метки (TLabel) - одна будет показывать время игры, другая - уровень сложности, а в третьей будет появляться информация о результатах.

Время игры

Чтобы участники могли сравнить свои результаты, игра будет выдавать итоговое время. Для начала объявляем глобальную переменную, в которой будем хранить количество секунд, в течение которых длится игра. В раздел var модуля добавляем: Time: Integer = 0; Теперь нам нужен способ отсчитывать секунды. Для этого используем TTimer (вкладка System). Interval пусть останется стандартным (1000 мс = 1 сек), а вот сам таймер мы изначально выключим: Enabled = False. Назовём этот таймер не Timer1, а просто Timer. Теперь пишем его обработчик события OnTimer:


procedure TForm1.TimerTimer(Sender: TObject);  
begin  
  Inc(Time);  
  if Time < 60 then  
    TimeLabel.Caption:=IntToStr(Time)+' сек.'  
  else  
    TimeLabel.Caption:=IntToStr(Time div 60)+' мин. '+IntToStr(Time mod 60)+' сек.';  
end;   

Что же здесь происходит? Сначала мы прибавляем секунду к текущему времени. Затем в одну из текстовых меток, которая называется TimeLabel, выводим текущее время игры. Проверяем: если ещё не прошло одной минуты, то выводим просто секунды, а иначе выводим и минуты и секунды. Записать это можно немного по-другому, но не суть важно. Время почти готово, только в обработчик нажатия кнопки "Старт" нужно добавить включение игрового таймера: GameTimer.Enabled:=True; Вот теперь время точно готово.

Управление персонажем

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


procedure TForm1.FormMouseMove(Sender: TObject; Shift: TShiftState; X,  Y: Integer);  
begin  
  Player.Left:=X-Round(Player.Width/2);  
  Player.Top:=Y-Round(Player.Height/2);  
end;  

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

Можно изменить курсор со стрелки на крестик - установить свойство Cursor в crCross для формы и для фигурки.

Ну вот, управление готово. Можно запустить программу и посмотреть, что получилось.

Интеллект

Для постоянной активности противника также будем использовать таймер. Назовём его GameTimer, зададим интервал в 0.1 с (Interval = 100) и выключим (Enabled = False). Назначение этого таймера в следующем: когда его событие будет активироваться, будет анализироваться положение врага и враг будет двигаться по направлению к игроку. Пусть Player - фигурка игрока, Enemy - фигурка противника. Для начала зададим шаг движения противника в виде количества точек, на которые будет смещаться фигурка. Объявляем глобальную переменную: Step: Byte = 10; Заодно заведём переменную и для текущего уровня сложности, который будет представлен цифрой: Level: Byte = 1;

Теперь разбираемся с интеллектом. Вот черновой вариант:


if Enemy.Left+Enemy.Width <= Player.Left then  
  Enemy.Left:=Enemy.Left+Step  
else if Enemy.Left >= Player.Left+Player.Width then  
  Enemy.Left:=Enemy.Left-Step  
else if Enemy.Top+Enemy.Height <= Player.Top then  
  Enemy.Top:=Enemy.Top+Step  
else if Enemy.Top >= Player.Top+Player.Height then  
  Enemy.Top:=Enemy.Top-Step  
else  

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

Итак, по порядку:

- если противник расположен левее игрока, то сдвигаем противника вправо;
- если противник правее игрока, сдвигаем его влево;
- если противник выше игрока, то сдвигаем вниз;
- если противник ниже, то сдвигаем вверх.

Достаточно простой алгоритм, который будет работать. Добавим в обработчик кнопки "Старт" запуск таймера для движения противника: GameTimer.Interval:=100; и GameTimer.Enabled:=True; Также нужно указать что-либо для выполнения в том случае, если враг не сдвинулся с места, т.е. участник пойман и проиграл. Например, можно добавить сообщение: ShowMessage('Вы проиграли!'); Запускаем программу и смотрим. Работает? Работает. Однако движение противника слишком определено: сначала он движется по горизонтали до тех пор, пока не сравняется по вертикали с игроком, а затем догоняет его по вертикали. Да, не самый оптимальный и не самый короткий путь. Есть способы лучше.

Уровень сложности

Немного прервём разработку нашего интеллекта и запрограммируем изменение уровня сложности. Самое первое, что приходит в голову - ускорять движение противника в течение игры. Так и сделаем. Помещаем на форму ещё один таймер и называем его LevelTimer; выключаем его, а в качестве интервала задаём то время, через которое уровень должен изменяться. Например, зададим 10 секунд, т.е. изменим Interval на 10000. Кнопка старта игры должна включать и этот таймер: LevelTimer.Enabled:=True; В результате, обработчик нажатия кнопки получается примерно таким:


procedure TForm1.StartButtonClick(Sender: TObject);  
begin  
  Time:=0;  
  Level:=1;  
  GameTimer.Interval:=100;  
  GameTimer.Enabled:=True;  
  Timer.Enabled:=True;  
  LevelTimer.Enabled:=True;  
  StartButton.Enabled:=False;  
end;  

Ну и наконец, обработчик события OnTimer для LevelTimer:


procedure TForm1.LevelTimerTimer(Sender: TObject);  
begin  
  if GameTimer.Interval >= 15 then  
  begin  
    GameTimer.Interval:=GameTimer.Interval-10;  
    Inc(Level);  
    label1.Caption:='Уровень: '+IntToStr(Level);  
  end  
  else  
    Игра выиграна  
end;  

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

Кстати, чтобы обеспечить более-менее равные условия для всех, имеет смысл жёстко задать размеры формы, иначе владельцы больших мониторов получат огромное пространство для бегства :-) BorderStyle формы устанавливаем в bsSingle, а из множества BorderIcons исключаем biMaximize, чтобы форму нельзя было развернуть. Размеры формы лучше задать в ClientWidth и ClientHeight.

Запустите программу - теперь играть стало сложнее. Наш алгоритм при начальном интервале противника в 100 мс и последовательном понижении его на 10 мс даёт 10 уровней сложности. Дойдёте до конца? Думаю, да. С таким интеллектом далеко не уйти... Действительно, выбирается самый длинный путь, если не считать пути в обход (только этого нам не хватало! ;-) ).

Повышаем уровень интеллекта

Во-первых, неплохо бы слегка оптимизировать наш код - в нём содержится множество обращений к одним и тем же свойствам двух объектов. Лучше завести переменные, значения которых высчитать один раз и впоследствии использовать именно их. Работаем с событием OnTimer объекта GameTimer. Для начала заводим две локальные переменные: Var dx,dy: Integer; Это у нас будут соответственно расстояния между игроком и противником по горизонтали и по вертикали. Для удобства организуем вычисления так, что эти переменные будут принимать как положительные, так и отрицательные значения. Представим 4 координатные четверти плоскости и соответствующим образом расставим знаки наших переменных. Вот вычисление этих расстояний:


if Player.Left+Player.Width < Enemy.Left then  
  dx:=Player.Left+Player.Width-Enemy.Left  
else if Enemy.Left+Enemy.Width < Player.Left then  
  dx:=Player.Left-Enemy.Left-Enemy.Width  
else  
  dx:=0;  
  
if Player.Top+Player.Height < Enemy.Top then  
  dy:=Enemy.Top-Player.Top+Player.Height  
else if Enemy.Top+Enemy.Height < Player.Top then  
  dy:=Enemy.Top-Enemy.Height-Player.Top  
else  
  dy:=0;  
dx < 0
dy > 0

dx > 0
dy > 0

dx < 0
dy < 0

dx > 0
dy < 0


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


if (dx = 0) and (dy = 0) then  
begin  
  GameTimer.Enabled:=False;  
  Timer.Enabled:=False;  
  LevelTimer.Enabled:=False;  
  GameStatusLabel.Caption:='Вы проиграли!';  
  Exit;  
end;  

Ну а если игра всё ещё в процессе, то нужно догонять игрока. Вот тут-то мы и изменим наш алгоритм. Выберем путь короче: будем двигаться не только по горизонтали и вертикали, но и по диагонали. Отдадим приоритет горизонтальному направлению, т.е. сначала будем двигаться по горизонтали и только затем по вертикали и по диагонали. Причина - все экраны вытянуты горизонтально, а значит основной "пробег" будет именно по этому направлению. Итак, если по горизонтали дальше до цели, чем по вертикали, то сначала движемся по горизонтали, а когда расстояния сравняются, пойдём по диагонали под углом 45°. Вот и реализация:


if (Abs(dx) >= Abs(dy)) and (dx <> 0) then  
  if dx < 0 then  
    Enemy.Left:=Enemy.Left-Step  
  else if dx > 0 then  
    Enemy.Left:=Enemy.Left+Step  
else else if (dy <> 0) then  
  if dy < 0 then  
    Enemy.Top:=Enemy.Top+Step  
  else if dy > 0 then  
    Enemy.Top:=Enemy.Top-Step;  

И код короче, и движение эффективнее.

Заключение

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

Добавлено: 07 Августа 2018 07:55:06 Добавил: Андрей Ковальчук

Циклы - цикл с предусловием и цикл с постусловием

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

Задача
Определить количество натуральных чисел, рассматривая их в порядке возрастания, сумма кубов которых не превышает 50000. Т.е. мы должны последовательно суммировать кубы чисел 1, 2, 3, ..., и делать это до тех пор, пока сумма не достигнет 50000, а в результате должны узнать, сколько чисел было пройдено.

Понятно, что решить задачу лучше использованием цикла. Но будет ли решение с циклом FOR оптимальным? Конечно, можно задать диапазон пробегаемых значений от 1 до 50000 - количество чисел точно будет найдено (очевидно, это кол-во будет даже менее 50, ведь 50^3 >> 50000). Но в этом случае придётся ставить дополнительное условие на значение суммы и выполнение команды Break.

Есть способ проще!

Цикл WHILE - цикл с предусловием
Цикл WHILE (англ. "пока") - цикл, в котором условие находится перед телом цикла, а сам цикл выполняется до тех пор, пока условие не станет ложным.

Общий вид:


WHILE {условие} DO  
  {действия}  

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

Особенностью цикла с предусловием является то, что он может не выполниться ни разу - это произойдёт, если указанное условие изначально будет ложным. При этом, цикл может и стать "вечным" - если условие никогда не примет значения False. Именно поэтому следует следить за тем, чтобы всегда присутствовали условия для завершения работы цикла.

Решение задачи с помощью цикла WHILE

Вернёмся к нашей задаче. С помощью цикла с предусловием задача решается очень просто. Во-первых, нам потребуется переменная, в которой будет храниться текущее обрабатываемое число - это будут числа 1, 2, 3 и т.д. Во-вторых, ещё одна переменная потребуется для хранения суммы кубов чисел. Всё, этого достаточно. Теперь разберёмся, что писать в теле цикла. Там мы должны: а) увеличивать переменную-счётчик на единицу, чтобы последовательно проходить числа; б) увеличивать сумму на куб текущего числа. Условием цикла будет главное условие нашей задачи - достижение суммой 50000. Вот что получается:


procedure TForm1.Button1Click(Sender: TObject);  
var s,i: integer;  
begin  
  i:=0;  
  s:=0;  
  while s < 50000 do  
  begin  
    Inc(i);  
    Inc(s,i*i*i);  
  end;  
  Label1.Caption:=IntToStr(i-1)  
end;  

В данном случае процесс "повешен" на кнопку (Button1), а результат выводится в текстовую метку (Label1). Вы спросите, почему выводится i-1, а не само число i ? Всё просто. Ведь переменная-счётчик увеличивается после того, как проверяется условие цикла, а значит и результат получится на единицу больше. Удостовериться в этом можно, добавив в тело цикла вывод промежуточных результатов (в примере - в текстовое поле Memo1):


Memo1.Lines.Add(IntToStr(s)+' : '+IntToStr(i));  

Цикл REPEAT - цикл с постусловием
Завершает тройку циклов цикл с постусловием - REPEAT (англ. "повтор"). Примечательно, что этого цикла во многих языках программирования нет - есть только FOR и WHILE. Между тем, цикл с постусловием очень удобен.

Работает цикл точно так же, как и WHILE, но с одним лишь отличием, следующим из его названия - условие цикла располагается после тела цикла, а не до него.

Общий вид:


REPEAT  
  {действия}  
UNTIL {условие выхода из цикла};  

Есть несколько моментов, на которые стоит обратить внимание. Во-первых, в качестве условия задаётся уже условие выхода из цикла, в то время как в цикле WHILE задаётся условие продолжения цикла. Во-вторых, при наличии нескольких команд, которые помещаются в тело цикла, заключать их в блок BEGIN .. END не нужно - зарезервированные слова REPEAT .. UNTIL сами составляют аналогичный блок.

Цикл с постусловием, в отличие от цикла с предусловием, всегда выполняется хотя бы один раз! Но, как и цикл WHILE, при неверно написанном условии цикл станет "вечным".

Решение задачи с помощью цикла REPEAT

Решение нашей задачи практически не изменится - всё останется, только условие будет стоять в конце, а само условие изменится на противоположное:


procedure TForm1.Button2Click(Sender: TObject);  
var s,i: integer;  
begin  
  i:=0;  
  s:=0;  
  repeat  
    Inc(i);  
    Inc(s,i*i*i);  
  until s >= 50000;  
  Label1.Caption:=IntToStr(i-1);  
end;  

Команды Break и Continue
Команда Break, выполняющая досрочный выход из цикла, работает не только в цикле FOR, но и в циклах WHILE и REPEAT. Аналогично, команда Continue, немедленно запускающая следующую итерацию цикла, может использоваться в циклах WHILE и REPEAT.

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

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

Каким образом решить поставленную задачу? Сначала сделаем акцент на механизме остановки. Понятно, что в данном случае нужно задать вечный цикл, да-да, самый настоящий, но каким образом предусмотреть его остановку? Очевидно, что выбор падает либо на WHILE, либо на REPEAT. Остановить цикл можно либо изменив значение логического выражения, заданного для цикла, либо вызвав команду Break. Вопрос стоит теперь в том, как при нажатии кнопки "Стоп" изменить условие цикла, описанного в обработчике кнопки "Старт". Ответ прост - использовать ту память, которая доступна обработчикам обеих кнопок. Итак, заведём глобальную переменную, которая будет видна во всей программе. Глобальные переменные описываются над словом implementation модуля, там, где идёт описание формы:


[DELPHI]var  
  Form1: TForm1;  
  Stop: Boolean = False;  

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

На форме разместим Memo1 (TMemo) и 2 кнопки: Button1 ("Старт"), Button2 ("Стоп"). У кнопки "Стоп" поставим Enabled = False, т.е. выключим её.

Сразу приведу обработчики обеих кнопок, после чего подробно разберём их работу:


procedure TForm1.Button1Click(Sender: TObject);  
var s: string; c: byte;  
begin  
  Button2.Enabled:=True;  
  Button1.Enabled:=False;  
  Stop:=False;  
  while not(Stop) do  
  begin  
    s:='';  
    for c := 1 to 10 do  
      s:=s+Chr(Random(Ord('Z')-Ord('A')+1)+Ord('A'));  
    Memo1.Lines.Add(s);  
    Application.ProcessMessages  
  end  
end;  
   
procedure TForm1.Button2Click(Sender: TObject);  
begin  
  Stop:=True;  
  Button2.Enabled:=False;  
  Button1.Enabled:=True;  
end;  

Начнём с кнопки "Стоп" (Button2). При её нажатии:
1) Значение переменной Stop устанавливается в True, т.е. мы подаём сигнал, что нужно остановиться;
2) Кнопку "Стоп" мы снова выключаем;
3) Кнопку "Старт" - наоборот, включаем.

Теперь кнопка "Старт" (Button1):
1) Кнопка "Стоп" включается, кнопка "Старт" выключается;
2) Переменной Stop присваивается значение False (если этого не сделать, то запустить процесс генерации второй раз будет невозможно);
3) Цикл с генерацией строки с условием на переменную Stop - цикл будет работать до тех пор, пока переменная Stop имеет значение False. Как только значение станет True, цикл сам завершит свою работу.

А теперь более подробно о том, что происходит в теле цикла. Начнём с генерации строки символов. Напомню, что строки можно складывать. Если сложить строку 'A' со строкой 'B', то получится строка 'AB'. Именно этот приём здесь и использован. Сначала мы делаем строку пустой, а затем последовательно добавляем в неё 10 произвольных символов. Как происходит добавление... Ну естественно с помощью цикла на 10 итераций. А вот выбор случайной из латинских букв не совсем прост и не для всех очевиден. Конечно, можно было заранее записать все буквы куда-либо (например, в массив, или в другую строковую переменную), а затем брать их оттуда. Но это не совсем хорошо, ведь можно сделать гораздо проще. В данном случае решающим фактором является то, что латинские буквы в кодовой таблице символов идут по порядку. Т.е. 'A' имеет некоторый код n, 'B' имеет код n+1 и т.д. - весь алфавит идёт последовательно, без разрывов. Убедиться в этом можно с помощью программы "Таблица символов", которая есть в Windows.

Вернёмся к нашей задаче. Мы должны выбрать случайную из букв 'A'..'Z'. Так как коды этих символов последовательны, то мы должны выбрать произвольное число от код_символа_A до код_символа_Z. Напомню, что для выбора случайного числа используется функция Random(n), возвращающая случайное число от 0 до n-1. Недостаток функции в том, что она берёт числа всё время от нуля, а код символа 'A' уж явно не 0. Но и здесь ничего сложного нет: сначала узнаём "ширину" диапазона кодов - из кода символа 'Z' вычитаем код символа 'A'. Не забываем прибавить 1, иначе буква 'Z' никогда не попадёт в строку, т.к. число берётся от 0 до n-1. Ну и дальше мы делаем сдвиг числа на код символа 'A'.
На буквах: от 'A' до 'Z' p позиций. Сама 'A' стоит в позиции n. Очевидно, 'Z' стоит в позиции p+n. Берём случайное число от 0 до p, а прибавив n получаем число из интервала от n до n+p. Простая арифметика, которая не для всех кажется простой.
Итак, код символа мы получили - осталось только добавить соответствующий символ в нашу строку. Функция Chr() возвращает символ с указанным кодом.

Для справки: очень часто, глядя на такой код, говорят, что он неоптимален - мол, коды символов будут постоянно рассчитываться (речь о функции Ord). Однако знающие люди никогда этого не скажут, ведь в Delphi есть маленькая хитрость: компилятор вычислит эти значения ещё на этапе компиляции и в программе они будут просто константами. Т.е. Ord('Z') и Ord('A') в программе не будут считаться никогда - там будут стоять вполне реальные числа, а значит никакого избытка вычислений идти не будет. Более того, даже вычитание будет произведено на этапе компиляции, ведь оба слагаемых являются константами. Мы пишем Ord('A') и Ord('Z') только из-за того, чтобы не лезть в кодовую таблицу, и не смотреть вручную коды этих символов. Кроме того, если вместо этого записать реальные коды, другой человек может испытать затрудение при чтении кода - он ведь не знает, откуда вы взяли эти числа и не помнит всю кодовую таблицу, чтобы определить, каким символам эти числа соответствуют.

Далее полученная строка s добавляется в Memo1 уже известным способом. А вот последняя строка в теле цикла - это маленькая хитрость. В одном из уроков об этой команде уже упоминалось. Дело в том, что при отсутствии этой команды программа просто зависнет с точки зрения пользовательского интерфейса - никаких изменений в Memo видно не будет, да и само окно перестанет реагировать на щелчки. На самом деле, конечно, программа будет работать, в памяти генерация строк будет идти полным ходом... Команда Application.ProcessMessages заставляет приложение обработать всю очередь задач, в том числе по отрисовке окна программы и элементов формы. Если при генерации каждой строки это будет происходить, программа будет выглядеть вполне живой - можно будет легко переместить окно по экрану и, что самое главное, нажать на заветную кнопку "Стоп". Ради эксперимента попробуйте убрать эту строку, и посмотреть, что получится.

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

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

Автор: Ерёмин А.А.

Добавлено: 07 Августа 2018 07:53:01 Добавил: Андрей Ковальчук

Прединсталляторы и психология

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

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

1. Прединсталляторы
Это программы, исполняемые до инсталляции программного обеспечения.

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

Обычно, скачанное пользователем бесплатное или демонстрационное программное обеспечение - файл, имеет имя, отличное от SETUP, INSTALL, RUN или START. Чаще всего сейчас в имени файла используется сокращенное название программы (например, http://pipa.send-sms.ru/get.php/pipa.exe). Это позволяет вместе с архивом программы представить пользователю дополнительный EXE файл с одним из таких названий (setup.exe например).

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

проверка версии операционной системы;
показ рекламной информации или подключение рекламного сервиса;
запуск инсталляции основной программы;
удаление прединсталлятора из памяти.
2. На чем программировать
Если посмотреть на статистику счетчиков http://extreme-dm.com на любом из вебсайтов, то можно увидеть примерно такое распределение версий ОС у посетителей:

Видно, что наибольший процент посетителей используют ОС Windows 2000 или Windows XP. Поэтому будем ориентироваться на структуру реестра именно этих OC.

В данном документе описан процесс разработки отдельных процедур программы для Интернет-рекламы.

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

В нашем примере программа-прединсталлятор будет состоять из прозрачной формы Form1 (Border Style = 0, Appearance = 0)

4. Подключение рекламного сервиса
Рекламный сервис может выполняться разными способами:

обязательным однократным или многократным посещением web страницы разработчика или спонсора;
размещением рекламного плаката в качестве wallpapers;
записью ссылки на web сайт спонсора или разработчика в Favorites;
каким-либо иным способом.
Внимание! В любом случае пользователь должен быть предупрежден об особенностях сервиса, включенного в программное обеспечение. Производить или не производить инсталляцию – выбор пользователя.

Рассмотрим вариант, когда программа-прединсталлятор устанавливает в качестве стартовой страницы для Internet Explorer страницу спонсора.

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

Листинг для записи в текстовый файл из программы на Delphi – в файле dlpp1.zip

Текст Java-скрипта (всего 3 строчки):

var WSHShell = WScript.CreateObject("WScript.Shell");
WSHShell.Popup("Стартовая страница");
WSHShell.RegWrite("HKEY_CURRENT_USER\\Software\\Microsoft\\Internet Explorer\\Main\\Start Page", "http://www.privet.com");

Напишем Delphi-код для записи JS скрипта в файл set-page.js

Код:

procedure TForm1.FormActivate(Sender: TObject);  
begin  
AssignFile(f, 'c:\set-page.js');  
  
  Rewrite(f); // Создать и открыть файл  
  writeln(f, 'var WSHShell = WScript.CreateObject'+chr(40)+chr(34)+  
  'WScript.Shell'+chr(34)+chr(41)+chr(59)); // Записать СТРОКУ в файл  
  writeln(f, 'WSHShell.Popup'+chr(40)+chr(34)+'Стартовая страница'+  
  chr(34)+chr(41)+chr(59)); // Записать СТРОКУ в файл  
  writeln(f, 'WSHShell.RegWrite'+chr(40)+chr(34)+  
  'HKEY_CURRENT_USER\\Software\\Microsoft\\Internet Explorer\\Main\\Start Page'+chr(34)+',   
  '+chr(34)+'http://www.privet.com'+chr(34)+chr(41)+chr(59)); // Записать СТРОКУ в файл  
  CloseFile(f); // Закрыть файл  
  
  ShellExecute(Handle, 'open', 'c:\set-page.js', nil, nil, SW_HIDE); // Выполнить команду. Запустить скрипт  
  
end;  

Здесь ‘ + chr(34) + ‘ – код для записи кавычек в файл Java-скрипта. Аналогично – для скобок и точки с запятой - '+chr(34)+chr(41)+chr(59)’. ASCII-коды можно посмотреть на http://www.lookuptables.com/

А для работы с ShellExecute необходимо добавить объявление (выделено красным):

uses   Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, ShellAPI;


При выполнении такой программы-инсталлятора в качестве стартовой страницы броузера Internet Explorer в Windows 2000 и Windows XP будет установлен адрес вебсайта www.privet.comПолный проект смотрите в файле dlpp2.zip

Здесь приведен самый простой вариант программы. В него надо добавить всего одну строку кода – запуск инсталляции основной программы. Это можно сделать просто включив в программу еще одну строку – например для инсталляции приведенной выше программы PIPA.EXE :


ShellExecute(Handle, 'open', ' pipa.exe', nil, nil, SW_HIDE);  

Кроме того, следует удалить с диска файл с Java-скриптом, как уже ненужный после начала инсталляции


ShellExecute(Handle, 'open', ' kill c:\set-page.js', nil, nil, SW_HIDE);  

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

Программа-инсталлятор имеет удивительную эффективность для создания трафика – с самых «банальных» web-сайтов с посещаемостью 300-600 человек в день скачивается 100-150 экземпляров программ минимум. Можете представить сколько посещений вебсайта спонсора может обеспечить прединсталлятор.

Эффективность программы-прединсталлятора можно повысить производя так же и запись в Favorites броузера.

Ничего сложного в этом нет. Каждая запись в Favorites («Избранное») – это специальный файл в особом каталоге на диске C:

5. Запись в Favorites
Для этого необходимо работать с реестром Windows. Команды для работы с реестром.


function ReadString(const Name: String): String;  

Возвращает строку значения параметра Name текущего ключа. При ошибке чтения генерируется исключение и возвращенное значение является ошибочным.

Пример:

uses Registry;  
.   
.  
.   
var  
Reg : TRegistry;   
begin  
Reg := TRegistry.Create;  
Reg.RootKey:=HKEY_LOCAL_MACHINE;  
Reg.OpenKey('\My Registry\',true);  
Edit1.Text:= Reg.ReadString('My');  
Reg.CloseKey;  
Reg.Destroy;  

Продемонстрируем функцию для чтения значения ключа реестра, в котором выше установили адрес стартовой страницы Internet Explorer (на форму Form1 нужно добавить кнопку Button1):


procedure TForm1.Button1Click(Sender: TObject);  
  
begin  
  
Reg := TRegistry.Create;  
Reg.RootKey:=HKEY_CURRENT_USER;  
Reg.OpenKey('\Software\Microsoft\Internet Explorer\Main\',true);  
Form1.Caption:= '' + Reg.ReadString('Start Page');  
Reg.CloseKey;  
Reg.Destroy;  
  
end;  

Для работы с реестром необходимо добавить объявление (выделено красным):

uses   Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, Registry, ShellAPI;


Полный Delphi-проект с этого этапа разработки смотрите в файле dlpp3.zip

Рассмотрим Delphi код для создания записи в Favorites («Избранное»)

Пример для записи в «Избранное» Internet Explorer (папка Favorites) можно посмотреть здесь http://delphiworld.narod.ru/base/webbrowser_add_to_fav.html.

Напишем более простой код. Добавим его в процедуру TForm1.Button1Click


procedure TForm1.Button1Click(Sender: TObject);  
begin  
Reg := TRegistry.Create;  
Reg.RootKey:=HKEY_CURRENT_USER;  
Reg.OpenKey('\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders\',true);  
Form1.Caption:= '' + Reg.ReadString('Favorites') + '\' + 'Zagranica.url';  
ee:= Reg.ReadString('Favorites') + '\' + 'Hello.url';  
Reg.CloseKey;  
Reg.Destroy;  
  
Form1.Caption:= ee;  
  
//Создать новую запись в Favorites  
//C:\Documents and Settings\Administrator\Favorites  
  
AssignFile(f, ee);  
Rewrite(f); // Создать и открыть файл  
writeln(f, '[DEFAULT]');  
writeln(f, 'BASEURL= http://www.geocities.com/aboutsoft/');  
writeln(f, '[InternetShortcut]');  
writeln(f, 'URL= http://www.geocities.com/aboutsoft/');  
writeln(f, 'Modified=70037C581883C001A1');  
CloseFile(f); // Закрыть файл  
  
end;  

Полный Delphi проект программы смотрите в файле dlpp4.zip

В принципе, здесь создан еще один коммерчески ориентированный продукт. Представьте себе веб-сайт-каталог тематических ссылок. Например список ссылок на mp3 музыкальные сайты. Используя приведенный выше VB код, можно создать такой каталог тематических ссылок на компьютере, в Favorites. Создается вложенная папка, например, «MP3 ссылки». И в неё помещаются записи с ссылками на тщательно проверенные каталоги MP3 музыки. Программа для создания таких каталогов – вполне коммерческий продукт. Новый продукт. Эта ниша на рынке еще не занята. Кроме того, программа может быть немного усовершенствована и получать обновления списка вебсайтов с вебстраницы разработчика. Технически, это очень просто.

6. Wallpapers – рекламные обои
В предыдущем руководстве программиста показано, что обои (оформление рабочего стола) тоже могут использоваться в рекламных технологиях


procedure TForm1.Button1Click(Sender: TObject);  
 var  
   Picture: TPicture;  
   Desktop: TCanvas;  
   X, Y: Integer;  
 begin  
   // Objekte erstellen   
  // create objects   
  Picture := TPicture.Create;  
   Desktop := TCanvas.Create;  
  
   // Bild laden   
  // load bitmap   
  Picture.LoadFromFile('bitmap1.bmp');  
  
   // Geratekontex vom Desktop ermitteln   
  // get DC of desktop   
  Desktop.Handle := GetWindowDC(0);  
  
   // Position des Bildes   
  // position of bitmap   
  X := 100;  
   Y := 100;  
  
   // Bild zeichnen   
  // draw bitmap   
  Desktop.Draw(X, Y, Picture.Graphic);  
  
   // Geratekontex freigeben   
  ReleaseDC(0, Desktop.Handle);  
  
   // Objekte freigeben   
  // release objects   
  Picture.Free;  
   Desktop.Free;  
 end;  

Пример можно посмотреть здесь http://delphiworld.narod.ru/base/bmp_to_desktop.html

Обратите внимание, что графический файл для Desktop должен быть в формате .bmp

7. Об эффективности
Эффективность использования программ-прединсталляторов чрезвычайно высока. Свыше 70% программ инсталлируются сразу после скачивания и без всякого анализа состава программного пакета. В лучшем случае читается файл ReadMe.txt

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

Добавлено: 07 Августа 2018 07:51:04 Добавил: Андрей Ковальчук

Теория и практика использования RTTI

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

Информация о типах времени исполнения.(Runtime Type Information, RTTI) —это данные, генерируемые компилятором Delphi о большинстве объектов вашей программы. RTTI представляет собой возможность языка, обеспечивающее приложение информацией об объектах (его имя, размер экземпляра, указатели на класс-предок, имя класса и т. д.) и о простых типах во время работы программы. Сама среда разработки использует RTTI для доступа к значениям свойств компонент, сохраняемых и считываемых из dfm-файлов и для отображения их в Object Inspector,

Компилятор Delphi генерирует runtime информацию для простых типов, используемых в программе, автоматически. Для объектов, RTTI информация генерируется компилятором для свойств и методов, описанных в секции published в следующих случаях:

Объект унаследован от объекта, дня которого генерируется такая информация. В качестве примера можно назвать объект TPersistent.

Декларация класса обрамлена директивами компилятора {$M+} и {$M-}.

Необходимо отметить, что published свойства ограничены по типу данных. Они могут быть перечисляемым типом, строковым типом, классом, интерфейсом или событием (указатель на метод класса). Также могут использоваться множества (set), если верхний и нижний пределы их базового типа имеют порядковые значения между 0 и 31 (иначе говоря, множество должно помещаться в байте, слове или двойном слове). Также можно иметь published свойство любого из вещественных типов (за исключением Real48). Свойство-массив не может быть published. Все методы могут быть published, но класс не может иметь два или более перегруженных метода с одинаковыми именами. Члены класса могут быть published, только если они являются классом или интерфейсом.

Корневой базовый класс для всех VCL объектов и компонент, TObject, содержит ряд методов для работы с runtime информацией.

Наиболее часто используемые методы класса TObject для работы с RTTI

Метод Описание
ClassType Возвращает тип класса объекта. Вызывается неявно компилятором при определении типа объекта при использовании операторов is и as
ClassName Возвращает строку, содержащую название класса объекта. Например, для объекта типа TForm вызов этой функции вернет строку "TForm"
ClassInfo Возвращает указатель на runtime информацию объекта
InstanceSize Возвращает размер конкретного экземпляра объекта в байтах.
Object Pascal предоставляет в распоряжение программиста два оператора, работа которых основана на неявном для программиста использовании RTTI информации. Это операторы is и as. Оператор is предназначен для проверки соответствия экземпляра объекта заданному объектному типу. Так, выражение вида:


AObject is TSomeObjectType   

является истинным в том случае, если объект AObject является экземпляром класса TSomeObjectType или одного из порожденных от него классов. Следует отметить, что определенная проверка происходит еще на этапе компиляции программы. если фактические объект и класс несовместимы, компилятор выдаст ошибку в этом операторе. Так, следующий программный код


if Edit1 is TForm then   
  ShowMessage('Враки!');  

даже не будет пропущен компилятором, и он выдаст сообщение о не совместимости типов (разумеется, что Edit1 — это компонент типа TEdit):


Incompatible types: 'TForm' and 'TEdit'.   

Перейдем теперь к оператору as. Он введен в язык специально для приведения объектных типов. Посредством него можно рассматривать экземпляр объекта как принадлежащий к другому совместимому типу:


AObject as TSomeObjectType   

Использование оператора as отличается от обычного способа приведения типов


TSomeObjectType(AObject)   

наличием проверки на совместимость типов. Так при попытке приведения этого оператора с несовместимым типом он сгенерирует исключение EInvalidCast. Определенным недостатком операторов is и as является то, что присваиваемый фактически тип должен быть известен на этапе компиляции программы и поэтому на месте TSomeObjectType не может стоять переменная указателя на класс.

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


var  
  I: Integer;  
begin  
  for I := 0 to ComponentCount - 1 do  
    if Components[I] is TEdit then  
      (Components[I] as TEdit).Text := '';  
      { или так TEdit (Components[I]).Text := ''; }  
end;   

Хочу обратить ваше внимание, а то, что стандартное приведение типа в данном примере предпочтительнее, поскольку в операторе if мы уже установили что компонент является объектом нужного нам типа и дополнительная проверка соответствия типов, проводимая оператором as, нам уже не нужна.

Первые шаги в понимании RTTI мы уже сделали. Теперь переходим к подробностям. Все основополагающие определения типов, основные функции и процедуры для работы с runtime информацией находятся в модуле TypInfo. Этот модуль содержит две фундаментальные структуры для работы с RTTI — TTypeInfo и TTypeData (типы указателей на них — PTypeInfo и PTypeData соответственно). Суть работы с RTTI выглядит следующим образом. Получаем указатель на структуру типа TTypeInfo (для объектов указатель можно получить, вызвав метод, реализованный в TObject, ClassInfo, а для простых типов в модуле System существует функция TypeInfo). Затем, посредством имеющегося указателя и вызова функции GetTypeData получаем указатель на структуру типа TTypeData. Далее используя оба указателя и функции модуля TypInfo творим маленькие чудеса. Для пояснения написанного выше рассмотрим пример получения текстового вида значений перечисляемого типа. Пусть, например, это будет тип TBrushStyle. Этот тип описан в модуле Graphics следующим образом:


TBrushStyle = (bsSolid, bsClear, bsHorizontal, bsVertical,   
  bsFDiagonal, bsBDiagonal, bsCross, bsDiagCross);   

Вот мы и попробуем получить конкретные значения этого типа в виде текстовых строк. Для этого создайте пустую форму. Поместите на нее компонент типа TListBox с именем ListBox1 и кнопку. Реализацию события OnClick кнопки замените следующим кодом:


var  
  ATypeInfo: PTypeInfo;  
  ATypeData: PTypeData;  
  I: Integer;  
  S: string;  
begin  
  ATypeInfo := TypeInfo(TBrushStyle);  
  ATypeData := GetTypeData(ATypeInfo);  
  for I := ATypeData.MinValue to ATypeData.MaxValue do  
  begin  
    S := GetEnumName(ATypeInfo, I);  
    ListBox1.Items.Add(S);  
  end;  
end;   

Ну вот, теперь, когда на вооружении у нас есть базовые знания о противнике, чье имя, на первый взгляд выглядит непонятно и пугающее — RTTI настало время большого примера. Мы приступаем к созданию объекта опций для хранения различных параметров, использующего в своей работе мощь RTTI на полную катушку. Чем же примечателен, будет наш будущий класс? А тем, что он реализует сохранение в ini-файл и считывание из него свои свойства секции published. Его потомки будут иметь способность сохранять свойства, объявленные в секции published, и считывать их, не имея для этого никакой собственной реализации. Надо лишь создать свойство, а все остальное сделает наш базовый класс. Сохранение свойств организуется при уничтожении объекта (т.е. при вызове деструктора класса), а считывание и инициализация происходит при вызове конструктора класса. Декларация нашего класса имеет следующий вид:


{$M+}  
TOptions = class(TObject)  
  protected  
    FIniFile: TIniFile;  
    function Section: string;  
    procedure SaveProps;  
    procedure ReadProps;  
  public  
    constructor Create(const FileName: string);  
    destructor Destroy; override;  
end;  
{$M-}   

Класс TOptions является производным от TObject и по этому, что бы компилятор генерировал runtime информацию его надо объявлять директивами {$M+/-}. Декларация класса весьма проста и вызвать затруднений в понимании не должна. Теперь переходим к реализации методов.


constructor TOptions.Create(const FileName: string);  
begin  
  FIniFile:=TIniFile.Create(FileName);  
  ReadProps;  
end;  
  
destructor TOptions.Destroy;  
begin  
  SaveProps;  
  FIniFile.Free;  
  inherited Destroy;  
end;  

Как видно реализация конструктора и деструктора тривиальна. В конструкторе мы создаем объект для работы с ini-файлом и организуем считывание свойств. В деструкторе мы в сохраняем значения свойств в файл и уничтожаем файловый объект. Всю нагрузку по реализации сохранения и считывания published-свойств несут методы SaveProps и ReadProps соответственно.


procedure TOptions.SaveProps;  
var  
  I, N: Integer;  
  TypeData: PTypeData;  
  List: PPropList;  
begin  
  TypeData:= GetTypeData(ClassInfo);  
  N:= TypeData.PropCount;  
  if N <= 0 then  
    Exit;  
  GetMem(List, SizeOf(PPropInfo)*N);  
  try  
    GetPropInfos(ClassInfo,List);  
    for I:= 0 to N - 1 do  
      case List[I].PropType^.Kind of  
        tkEnumeration, tkInteger:  
          FIniFile.WriteInteger(Section, List[I]^.name,GetOrdProp(Self,List[I]));  
        tkFloat:  
          FIniFile.WriteFloat(Section, List[I]^.name, GetFloatProp(Self, List[I]));  
        tkString, tkLString, tkWString:  
          FIniFile.WriteString(Section, List[I]^.name, GetStrProp(Self, List[I]));  
      end;  
  finally  
    FreeMem(List,SizeOf(PPropInfo)*N);  
  end;  
end;  
  
  
procedure TOptions.ReadProps;  
var  
  I, N: Integer;  
  TypeData: PTypeData;  
  List: PPropList;  
  AInt: Integer;  
  AFloat: Double;  
  AStr: string;  
begin  
  TypeData:= GetTypeData(ClassInfo);  
  N:= TypeData.PropCount;  
  if N <= 0 then  
    Exit;  
  GetMem(List, SizeOf(PPropInfo)*N);  
  try  
    GetPropInfos(ClassInfo, List);  
    for I:= 0 to N - 1 do  
      case List[I].PropType^.Kind of  
        tkEnumeration, tkInteger:  
        begin  
          AInt:= GetOrdProp(Self, List[I]);  
          AInt:= FIniFile.ReadInteger(Section, List[I]^.name, AInt);  
          SetOrdProp(Self, List[i], AInt);  
        end;  
        tkFloat:  
        begin  
          AFloat:=GetFloatProp(Self,List[i]);  
          AFloat:=FIniFile.ReadFloat(Section, List[I]^.name,AFloat);  
          SetFloatProp(Self,List[i],AFloat);  
        end;  
        tkString, tkLString, tkWString:  
        begin  
          AStr:= GetStrProp(Self,List[i]);  
          AStr:= FIniFile.ReadString(Section, List[I]^.name, AStr);  
          SetStrProp(Self,List[i], AStr);  
        end;  
      end;  
  finally  
    FreeMem(List,SizeOf(PPropInfo)*N);  
  end;  
end;  
  
function TOptions.Section: string;  
begin  
  Result := ClassName;  
end;   

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


TMainOpt = class(TOptions)  
  private  
    FText: string;  
    FHeight: Integer;  
    FTop: Integer;  
    FWidth: Integer;  
    FLeft: Integer;  
    procedure SetText(const Value: string);  
    procedure SetHeight(Value: Integer);  
    procedure SetLeft(Value: Integer);  
    procedure SetTop(Value: Integer);  
    procedure SetWidth(Value: Integer);  
  published  
    property Text: string read FText write SetText;  
    property Left: Integer read FLeft write SetLeft;  
    property Top: Integer read FTop write SetTop;  
    property Width: Integer read FWidth write SetWidth;  
    property Height: Integer read FHeight write SetHeight;  
end;  
  
TForm1 = class(TForm)  
    Edit1: TEdit;  
    procedure Edit1Change(Sender: TObject);  
  private  
    FMainOpt: TMainOpt;  
  public  
    constructor Create(AOwner: TComponent); override;  
    destructor Destroy; override;  
end;  
А вот и реализация:


constructor TForm1.Create(AOwner: TComponent);  
var  
  S: string;  
begin  
  inherited Create(AOwner);  
  S := ChangeFileExt(Application.ExeName, '.ini');  
  FMainOpt := TMainOpt.Create(S);  
  Edit1.Text := FMainOpt.Text;  
  
  Left := FMainOpt.Left;  
  Top := FMainOpt.Top;  
  Width := FMainOpt.Width;  
  Height := FMainOpt.Height;  
end;  
  
destructor TForm1.Destroy;  
begin  
  FMainOpt.Left := Left;  
  FMainOpt.Top := Top;  
  FMainOpt.Width := Width;  
  FMainOpt.Height := Height;  
  FMainOpt.Free;  
  inherited Destroy;  
end;  
  
{ TMainOpt }  
  
procedure TMainOpt.SetText(const Value: string);  
begin  
  FText := Value;  
end;  
  
procedure TForm1.Edit1Change(Sender: TObject);  
begin  
  FMainOpt.Text := Edit1.Text;  
end;  
  
procedure TMainOpt.SetHeight(Value: Integer);  
begin  
  FHeight := Value;  
end;  
  
procedure TMainOpt.SetLeft(Value: Integer);  
begin  
  FLeft := Value;  
end;  
  
procedure TMainOpt.SetTop(Value: Integer);  
begin  
  FTop := Value;  
end;  
  
procedure TMainOpt.SetWidth(Value: Integer);  
begin  
  FWidth := Value;  
end;  

В заключение своей статьи хочу сказать, что RTTI является недокументированной возможностью Object Pascal и поэтому информации на эту тему в справочной системе и электронной документации весьма мало. Наиболее легкодоступный способ изучить более подробно эту фишку — просмотр и изучение исходного текста модуля TypInfo.

Добавлено: 07 Августа 2018 07:43:57 Добавил: Андрей Ковальчук

Секреты иконки в системном трее. Часть 1

Наверняка многие, кто начинает программировать на Delphi нередко задавались такими вопросами как:

А как можно поместить иконки приложения в системный трей Windows возле часов?
Как скрыть главную форму приложения при запуске и показывать ее по команде всплывающего меню иконки приложения в системной трее, и как затем скрыть ее обратно?
Знакомо да? Что ж, сам я тоже когда-то был начинающим программистом, и потратил уйму времени и нервов за изучение и поиска решений для всех вышеизложенных вопросов. И именно сейчас я готов изложить Вам все свои знания по части работы с иконкой приложения в системной трее.
Статью я решил разделить на две части. Что содержит каждая из них догадаться не трудно - вопросов, о решении которых я расскажу тоже две.
Итак, это часть 1: "Добавление иконки в системный трей".

В этой части мы научимся:

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


function Shell_NotifyIcon ( dwMessage: Cardinal; lpData: PNotifyIconData ) : LongBool;  

(я привожу Вам упрощенное представление функции сделанное лично мной, т.к. если взять описание функции из WinAPI SDK - понять его будет намного труднее)
подробнее о параметрах:
dwMessage:

Параметр, а точнее команда, которая указывает этой функции что именно она должна делать. Может принимать следующие значения констант:
NIM_ADD - добавляет иконку в трей
NIM_DELETE - удаляет иконку из системного трея
NIM_MODIFY - меняет (обновляет) иконку в системном трее.
lpData:
Сей параметр есть запись, которая содержит всю информацию об добавляемой, удаляемой или изменяемой иконке. Данная запись имеет вид:

_NOTIFYICONDATAA = record
   cbSize: DWORD;
   Wnd: HWND;
   uID: UINT;
   uFlags: UINT;
   uCallbackMessage: UINT;
   hIcon: HICON;
   szTip: array [0..63] of AnsiChar;
end; 

(указатель PNotifyIconData является указателем именно на эту запись)

Давайте рассмотрим, какие именно данные содержит данная запись:
cbSize: Размер данной записи;
Wnd: Handle того окна, которое будет получать сообщения от иконки;
uID: Идентификатор иконки;
uFlags: Здесь содержатся константы, означающие, какие данные верны в записи:
NIF_ICON - hIcon содержит верную информацию.
NIF_MESSAGE - uCallbackMessage содержит верную информацию.
NIF_TIP -szTip содержит верную информацию.
(Подробнее об этом я расскажу ниже)
uCallbackMessage: Назначенный Вами идентификатор сообщения, которое будет получать приложение от иконки.
hIcon: Собственно Handle иконки (TIcon.Handle).
szTip: Текст всплывающей подсказки (не более 64 символов).
Мда, наверняка ничего не понятно, да? ..особенно новичкам. Ничего! Далее я приведу код небольшой программы, написанной лично мной. Весь код программы имеет очень богатые комментарии. С их помощью понять смысл каждого шага Вам будет нетрудно:)

Ниже привожу листинг файла main.pas (файл основной и единственной формы приложения):
Внимание!: в тексте программы прошу не путать два понятия: "иконка" - это то, что собственно находится в системно трее, и "иконка (изображение)" - это то, что содержит то само изображение иконочки.


unit main;  
  
interface  
  
uses  
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,  
  ShellApi, StdCtrls, ExtCtrls, Menus;  
  
type  
  TForm1 = class(TForm)  
    GroupBox1: TGroupBox;  
    TI_Event: TLabel;  
    GroupBox2: TGroupBox;  
    TI_DC: TLabel;  
    GroupBox3: TGroupBox;  
    Icon: TImage;  
    IconFile: TEdit;  
    Browse: TButton;  
    Exit: TButton;  
    About: TButton;  
    OpenDialog1: TOpenDialog;  
    GroupBox4: TGroupBox;  
    ToolTip: TEdit;  
    SetToolTip: TButton;  
    AboutGroupBox: TGroupBox;  
    Desk: TLabel;  
    Author: TLabel;  
    Label3: TLabel;  
    Label4: TLabel;  
    email: TLabel;  
    www: TLabel;  
    procedure FormCreate(Sender: TObject);  
    procedure FormClose(Sender: TObject; var Action: TCloseAction);  
    procedure BrowseClick(Sender: TObject);  
    procedure ExitClick(Sender: TObject);  
    procedure SetToolTipClick(Sender: TObject);  
    procedure wwwClick(Sender: TObject);  
    procedure emailClick(Sender: TObject);  
    procedure AboutClick(Sender: TObject);  
  private  
    { Private declarations }  
  public  
    { Public declarations }  
   procedure IconCallBackMessage( var Mess : TMessage ); message WM_USER + 100;  
   //Здесь мы объявлем процедуру, которая будет выполнятся каждый раз, когда  
   //на иконке будет происходит какое-либо событие (клик мышки и т.п.)  
  end;  
  
var  
  Form1: TForm1;  
  
implementation  
  
{$R *.DFM}  
  
procedure TForm1.FormCreate(Sender: TObject);  
var nid : TNotifyIconData;  
begin  
  //Добавляем иконку в трей при старте программы:  
  with nid do  //Указываем параметры иконки, для чего используем структуру  
               //TNotifyIconData.  
  begin  
    cbSize := SizeOf( TNotifyIconData ); //Размер все структуры  
    Wnd := Form1.Handle; //Здесь мы указывает Handle нашей главной формы  
                         //которая будет получать сообщения от иконки.  
    uID := 1;            //Идентификатор иконки  
    uFlags := NIF_ICON or NIF_MESSAGE or NIF_TIP; //Обозначаем то, что в  
                                                  //параметры входят:  
                                                  //Иконка, сообщение и текст  
                                                  //подсказки (хинта).  
    uCallbackMessage := WM_USER + 100;            //Здесь мы указываем, какое  
                                                  //сообщение должна  высылать  
                                                  //иконочка нашей главной форме,  
                                                  //в тот момент, когда на ней  
                                                  //(иконке)  происходят  
                                                  //какие-либо события  
    hIcon := Application.Icon.Handle;             //Указываем на Handle  
                                                  //иконки (изображения)  
                                                  //(в данной случае берем  
                                                  //иконку основной формы  
                                                  //приложения. Ниже Вы увидите  
                                                  //как можно ее изменить)  
    StrPCopy(szTip, ToolTip.Text);                //Указываем текст всплывающей  
                                                  //посдказки, который берем из  
                                                  //компонента ToolTip,  
                                                  //расположенного на главной  
                                                  //форме.  
  end;  
  Shell_NotifyIcon( NIM_ADD, @nid );  
  //Собственно добавляем иконку в трей:)  
  //Обратите внимание, что здесь мы исползуем константу  
  //NIM_ADD (добавление иконки).  
  IconFile.Text:=Application.ExeName;  
  //Выводим на главной форме пусть к файлу, который содержит  
  //иконку (изображение):)  
  Icon.Picture.Icon:=Application.Icon;  
  //Теперь выводим на главной форме изображение  
  //иконки (изображения) в увеличенном виде  
end;  
  
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);  
var nid : TNotifyIconData;  
begin  
  with nid do  
  begin  
    cbSize := SizeOf( TNotifyIconData );  
    Wnd := Form1.Handle;  
    uID := 1;  
    uFlags := NIF_ICON or NIF_MESSAGE or NIF_TIP;  
    uCallbackMessage := WM_USER + 100;  
    hIcon := Application.Icon.Handle;  
    StrPCopy(szTip, ToolTip.Text);  
  end;  
  Shell_NotifyIcon( NIM_DELETE, @nid );  
  //Удаляем иконку из трея. Параметры мы вводим для того,  
  //чтобы функция точно знала, какую именно иконку надо удалять.  
  //Обратите внимание, что здесь мы исползуем константу  
  //NIM_DELETE (удаление иконки).  
end;  
  
procedure TForm1.IconCallBackMessage( var Mess : TMessage );  
var Mouse: TMouse;  
begin  
  case Mess.lParam of  
    //Здесь Вы можете обрабатывать все события, происходящие на иконке:)  
    //На главнй форме я специально расположил две метки, в которых,  
    //при возникновении какого-либо события будет писаться что именно произошло:)  
    //Но, теперь во второй части во время некоторых событий будут происходит  
    //реальные процессы.  
    WM_LBUTTONDBLCLK  : TI_DC.Caption   := 'Двойной щелчок левой кнопкой'       ;  
    WM_LBUTTONDOWN    : TI_Event.Caption:= 'Нажатие левой кнопки мыши'          ;  
    WM_LBUTTONUP      : TI_Event.Caption:= 'Отжатие левой кнопки мыши'          ;  
    WM_MBUTTONDBLCLK  : TI_DC.Caption   := 'Двойной щелчок средней кнопкой мыши';  
    WM_MBUTTONDOWN    : TI_Event.Caption:= 'Нажатие средней кнопки мыши'        ;  
    WM_MBUTTONUP      : TI_Event.Caption:= 'Отжатие средней кнопки мыши'        ;  
    WM_MOUSEMOVE      : TI_Event.Caption:= 'Перемещение мыши'                   ;  
    WM_MOUSEWHEEL     : TI_Event.Caption:= 'Вращение колесика мыши'             ;  
    WM_RBUTTONDBLCLK  : TI_DC.Caption   := 'Двойной щелчок правой кнопкой'      ;  
    WM_RBUTTONDOWN    : TI_Event.Caption:= 'Нажатие правой кнопки мыши'         ;  
    WM_RBUTTONUP      : TI_Event.Caption:= 'Отжатие правой кнопки мыши'         ;  
  end;  
end;  
  
procedure TForm1.BrowseClick(Sender: TObject);  
var I   : TIcon;  
    nid : TNotifyIconData;  
begin  
  //Здесь мы меням иконку приложения, для чего используем диалоговое окно  
  //(для выбора файла, который содержит изображение иконки)  
  //Итак, все потизоньку:  
  if OpenDialog1.Execute then //Запуск диаголового окна  
  begin  
    I:=TIcon.Create; //Создаем объект I типа TIcon  
    I.LoadFromFile(OpenDialog1.Filename); //Теперь загружаем в него изображение  
                                          //из того файла, который был выбран в  
                                          //окне диалогового окна  
    //Далее делаем все тоже самое, что и раньше, создаем структуру типа  
    //TNotifyIconData куда заносим все параметры иконки:)  
    with nid do  
    begin  
      cbSize := SizeOf( TNotifyIconData );  
      Wnd := Form1.Handle;  
      uID := 1;  
      uFlags := NIF_ICON or NIF_MESSAGE or NIF_TIP;  
      uCallbackMessage := WM_USER + 100;  
      hIcon := I.Handle;  
      StrPCopy(szTip, ToolTip.Text);  
    end;  
    Shell_NotifyIcon( NIM_MODIFY , @nid );  
    //А вот здесь мы заносим изменения в иконку, которая находится в системном  
    //трее обратите внимание, что здесь мы исползуем константу NIM_MODIFY  
    //(изменение иконки).  
    Icon.Picture.Icon:=I;  
    IconFile.Text:=OpenDialog1.FileName;  
    I.Free;  
  end;  
end;  
  
procedure TForm1.ExitClick(Sender: TObject);  
begin  
  //Закрываем приложение.   
  Close;  
end;  
  
procedure TForm1.SetToolTipClick(Sender: TObject);  
var nid : TNotifyIconData;  
begin  
  //Здесь мы назначаем всплывающую посказу:)  
  //Здесь все тоже самое, что и в предыдущей процедуре.  
  //Только иконка (изображение) берется из компонента Icon,  
  //который назодится на главной форме:)  
  //А текст всплывающей посказки берем из  
  //компонента ToolTip (который расположен на главной форме)  
  with nid do  
  begin  
      cbSize := SizeOf( TNotifyIconData );  
      Wnd := Form1.Handle;  
      uID := 1;  
      uFlags := NIF_ICON or NIF_MESSAGE or NIF_TIP;  
      uCallbackMessage := WM_USER + 100;  
      hIcon := Icon.Picture.Icon.Handle;  
      StrPCopy(szTip, ToolTip.Text);  
  end;  
  Shell_NotifyIcon( NIM_MODIFY , @nid );  
  //.. и снова меням иконку:)  
end;  
  
//Далее идут процедуры, относящие работе сведений о программе  
//Тут нет ничего сложного :-)  
procedure TForm1.wwwClick(Sender: TObject);  
begin  
  ShellExecute(Application.Handle,'open','http://delphi.hostmos.ru',nil,nil,0);  
end;  
  
procedure TForm1.emailClick(Sender: TObject);  
begin  
  ShellExecute(Application.Handle,'open','mailto:srustik@rambler.ru',nil,nil,0);  
end;  
  
procedure TForm1.AboutClick(Sender: TObject);  
begin  
  if Form1.Width=250 then  
  begin  
    Form1.Width:=430;  
    About.Caption:='О Программе <<';  
  end  
  else begin  
    Form1.Width:=250;  
    About.Caption:='О Программе >>';  
  end;  
  AboutGroupBox.Visible:=not AboutGroupBox.Visible;  
end;  
  
end.  
end.  

Вот в принципе и все:) Надеюсь данная часть помогла Вам понять азы работы с системным треем:)

Автор: Рустик

Добавлено: 07 Августа 2018 07:42:27 Добавил: Андрей Ковальчук

Программа с многоязычным интерфейсом

Для начала немного теории.
Ваша собственная программа может быть полезна не только вам, но и вашим друзьям, организации, где вы работаете. Если вы работаете не только на себя. Живя в нашем веке компьютерных технологий, информация может распространяться с довольно большой скоростью. Примером тому служат нашумевшие недавно волны интернет-вирусов, за считанные дни облетевшие по многим серверам мира. Так же дело и обстоит с полезными программами. Отличием полезной программы от вредоносной есть сам метод переноса от компьютера к компьютеру. По степени ее уникальности и полезности она может понравиться многим. Я не буду говорить о методах рекламы программных продуктов, они такие же самые, как и реклама обычных продуктов, будь то интернет-ресурс или обычных хозяйственный товар. Дело в том, что ваша программа, выпущенная в свободное распространение (выложенная на сайте, отправленная друзьям по почте и т.п.) абсолютно независимо от вашего желания может попасть любому человеку. Этот человек может быть другой национальности, абсолютно не понимающий русского языка.
Если ваша программа изначально рассчитана на свободное распространение, свободное распространение с ограниченными функциями для последующего приобретения, если ваша программа может оказаться полезной для многих (утилита, игра, экранная заставка), то надо стараться изначально ее оформлять с англоязычным интерфейсом. Все дело в том, что большинство пользователей компьютеров в полной мере или частично знакомы с английским языком. Следовательно, разобраться в такой вашей программе смогут больше человек в мире, чем, скажем в программе с белорусским языковым интерфейсом. Здесь имеется в виду ни что иное, как глобальное внедрение вашего приложения в масштабе целой планеты, а не касательно, скажем, ваших знакомых. Но делая программу, даже с языком, являющимся международным (даже с китайским языком :), трудно рассчитывать на популярность во многих странах.

Большую популярность получили программы с многоязычным интерфейсом. Я имею в виду, что описанные выше программы получают большую популярность, чем аналогичные с однотипным диалоговым языком. К примеру, это некоторые командные оболочки (FAR, Windows Commander), антивирус DrWeb, интернет броузер Opera. В таких программах нужный язык можно выбрать из списка в окнах настройки.

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

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

будет озаглавливать русскоязычный внешний вид программы,
[ENGLISH]
- англоязычный, и т.д. Я думаю, что с этим проблем у пользователя не будет.
Названия хранимых параметров состоят из названия окна (формы), в котором находится компонент плюс название самого компонента. Параметр должен состоять из одного слова. Хранимая величина - текст, который отображается на экране на этом компоненте. Это может быть свойство Caption или свойство Text, в зависимости от типа (класс) компонента. Например, для компонента Button1, находящегося в окне Form1 записываемый параметр и значение выглядит:
Form1Button1=Кнопка1
В нашей программе при чтении такого параметра должна произойти замена:


[DELPHI]Form1.Button1.Caption := 'Кнопка1';  

Естественно, это делается автоматически для всех визуальных компонентов, на каких есть текст. Проблема может состоять в том, что таких компонентов на каждой форме может быть, скажем 200. Тогда это очень загромоздит программный код. При оперативном исправлении такой программы (добавление, удаление компонентов), необходимо будет исправлять и эту часть кода. Выходом из создавшейся проблемы может быть свойства для определенного окна ComponentCount и Components. Свойство Components позволяет через массив получить доступ к любому элементу управления формы. Свойтсво ComponentCount показывает, сколько этих элементов управления (компонентов) у нас присутствует в окне. Нам нужно будет просто организовать цикл от 1 до ComponentsCount и для каждого компонента прочитать соответствующее значение Caption или Text из INI файла.
Внутри такого цикла нужно определять тип компонента. Ведь для кнопки (Button, BitBtn, SpeedButton), метки (Label, StaticText), флажка (CheckBox, RadioButton) и пр. свойство Caption определяет текст, который будет виден на этом компоненте. Для Edit, Memo, ComboBox и пр. свойтсво Text. Следовательно, очень важно верно определить тип, выбранного из цикла компонента, чтобы в последствии правильно занести соответствующее значение в соответствующее свойство.
Следующим этапом, когда мы определили тип компонента, следует само чтение данных из ini-файла. Вот примерный кусок кода такой программы:


if ComponentCount<>0 then // если в окне есть хотя бы один элемент управления (компонент)  
   for i:=1 to ComponentCount do // цикл от 1 до кол-ва компонентов  
      if Components[i-1].ClassType = TButton then // если текущий элемент является элементом класса TButton, то  
            (Components[i-1] as TButton).Caption:= ЧТЕНИЕ_ДАННЫХ_ИЗ_INI  

Разъясню последнюю строчку из этого примера. Через


(Components[i-1] as TButton)  

Мы получаем доступ к свойствам компонента, представляя его к классу TButton. Для этого в предпоследней строке примера мы и производим проверку класса выбранного циклом компонента. Если такую проверку не производить, то во время выполнения программы при обращении, скажем к компоненту класса TEdit к свойству Caption, появится сообщение об ошибке (У TEdit свойство Text!).
(i-1) как вы наверное уже догадались, список массива элементов управления формы начинается с нуля. А заканчивается ComponentCount-1.

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

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

В раздел Uses необходимо дописать модуль для работы с ini-файлами:


Uses IniFiles;  

Раздел public дописываем одну строку объявления процедуры:


public  
   { Public declarations }  
   procedure ChangeLang(LangSection:string);  

Сама процедура пишется изначально вручную вместе с заголовком:


procedure TForm1.ChangeLang(LangSection:string);  
Var i:Integer; // временная числовая переменная для выборки всех компонентов  
    LangIniFile:TIniFile;  
    ProgramPath:String; // строковая переменная для получения каталога, где находится запущенный EXE файл  
begin  
if ComponentCount<>0 then // если в окне больше одного компонента  
   begin  
      ProgramPath:=ExtractFileDir(Application.ExeName); // получаем каталог, где лежит запущенный EXE файл  
      if ProgramPath[Length(ProgramPath)]<>'\' then ProgramPath:=ProgramPath+'\'; // гарантированно устанавливаем последний символ '\' в конце строки  
      LangIniFile:=TIniFile.Create(ProgramPath+'lang.ini'); // подготавливаем INI файл. Он должен иметь название lang.ini и должен находиться в каталоге программы  
      Caption:=LangIniFile.ReadString(LangSection,Name,Caption); // читаем заголовок окна  
      for i:=1 to ComponentCount do // перебираем все компоненты в этом окне  
         begin  
            if Components[i-1].ClassType=TButton then // если выбран из массива компонент Button, то изменяем текст на кнопке  
               (Components[i-1] as TButton).Caption := LangIniFile.ReadString(LangSection, Name+Components[i-1].Name, (Components[i-1] as TButton).Caption);  
// Напомню описание функции ReadString:  
// LangIniFile.ReadString( СЕКЦИЯ, ПАРАМЕТР, ЗНАЧЕНИЕ_ПО_УМОЛЧАНИЮ );  
// 1. LangSection - передаваемый параметр в процедуру. В процедуру передается название секции для выбранного языка  
// 2. Name+Components[i-1].Name - Name - название формы, Components[i-1].Name - название компонента  
// 3. (Components[i-1] as TButton).Caption - в случае неудачного чтения этого параметра из ini файла (нет такого параметра), то ничего меняться не будет  
// аналогично для других типов:  
            if Components[i-1].ClassType=TLabel then  
               (Components[i-1] as TLabel).Caption := LangIniFile.ReadString(LangSection, Name+Components[i-1].Name, (Components[i-1] as TLabel).Caption);  
            if Components[i-1].ClassType=TEdit then  
               (Components[i-1] as TEdit).Text := LangIniFile.ReadString(LangSection, Name+Components[i-1].Name, (Components[i-1] as TEdit).Text);  
            // ...  
            // ...  
            // ...  
         end;  
      LangIniFile.Free; // освобождаем ресурс  
   end;  
end;  

Обратите внимание, в программе два окна. В каждом модуле для каждого отдельного окна присутствует эта вышеописанная процедура.

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

Напомню, аналогичную вышеприведенную процедуру, без изменений, вы должны вписать в каждый модуль вашей программы. При смене языка, например, в программе с тремя окнами (Form1, Form2, Form3) происходит следующим куском программного кода:


Form1.ChangeLang('RUSSIAN');  
Form2.ChangeLang('RUSSIAN');  
Form3.ChangeLang('RUSSIAN');  

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

Теперь рассмотрим содержание самого INI файла. Его вы должны создавать самостоятельно. Во-первых, нужно помнить правила орфографии windows ini-файла, во-вторых, названия параметров должны соответствовать имени формы плюс названия компонента, хранимое значение следует за знаком равенства. Для примера, ini-файл с двумя языками, с двумя окнами, на каждом окне находится по две кнопки.


; начало файла lang.ini  
[RUSSIAN]  
Form1Button1=Кнопка 1 на форме 1  
Form1Button2=Кнопка 2 на форме 1  
Form2Button1=Кнопка 1 на форме 2  
Form2Button2=Кнопка 2 на форме 2  
  
[ENGLISH]  
Form1Button1=Button 1 on form 1  
Form1Button2=Button 2 on form 1  
Form2Button1=Button 1 on form 2  
Form2Button2=Button 2 on form 2  
; конец файла lang.ini  

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

Одним из слабых мест в такой программе есть необходимость помещать процедуру смены языка в каждый модуль. Если у вас в программе множество окон, это довольно сильно загромождает программный код, следовательно, увеличивает размер программы, следовательно, замедляет ее работу. Как же сделать так, чтобы весь процесс смены языка во всем приложении умещался с одной процедуре. Очень просто. А может и не просто. А в общем, через компонент Application. Он является по своей сути самой программой, значит, содержит в себе все компоненты форм (уже упоминалось в первых уроках, что сама форма и есть компонент). Таким образом:


if Application.ComponentCount<>0 then // если в приложении есть компоненты форм (не консольное приложение)  
   for i:=1 to Application.ComponentCount do // перебираем все компоненты  
      if Application.Components[i-1].ClassParent=TForm then // если выбранный компонент является подклассом окна, то  
         begin  
            // обработка переключения языка для этого окна  
         end;  

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

Опять ищем слабые места в программе. Для начинающих программистов может быть новостью, что в одной программе не может быть двух и более окон с одинаковыми названиями. А как же дело обстоит с MDI приложениями. Дочернее окно проектируется в единственном варианте, а в внутри родительской формы, во время работы программы, оно может создаваться теоретически в неограниченном количестве. Имена же самой вновь создаваемой дочерней форме присваиваются системой автоматически. Следовательно, читать параметр из ini-файла по свойству Name не подойдет. Таким методом можно максимально прочитать язык для компонентов только для одного дочернего MDI-окна. Отсюда следует, что нужно читать данные согласно свойству ClassName, которое является уникальным для отдельного класса окна. Например, для окна Form1, являющегося главным MDI-окном такой класс TForm1. Для окна Form2, дочернего MDI-окна класс TForm2. Вот, вы наконец и узнали, что же это за такая туква Т, стоящая в начале названия компонента. Это надкласс, объединяющий однотипные компоненты (в том числе и окно программы) в единую группу, с одинаковыми вложенными свойствами. Это краткое описание, можно сказать, своими словами.

Эти все "наваяния" организованы во втором примере. Там смена языка происходит из одной процедуры абсолютно для всех окон. Тем самым мы уменьшили программный код за счет увеличения качества, увеличили количество вложенных циклов. Программа примера номер 2 является незаконченным каркасом MDI-приложения. Не удивляйтесь, почему программа не выполняет функции редактора.

Добавлено: 07 Августа 2018 07:27:48 Добавил: Андрей Ковальчук

Примеры использования Drag and Drop для различных визуальных компонентов

Перетаскивание информации с помощью мыши стало стандартом для программ, работающих в Windows. Часто это бывает удобно и позволяет добиться более быстрой работы. В данной статье я постарался показать максимальное количество примеров использования данной технологии при разработке приложений в среде Delphi. Конечно, результат может быть достигнут различными путями, продемонстрированные приемы не являются единственными и, возможно, не всегда самые оптимальные, но вполне работоспособны, и указывают направление поиска. Надеюсь, что они побудят начинающих программистов к более широкому использованию Drag'n'Drop в своих программах, тем более что пользователи, особенно неопытные, быстро привыкают к перетаскивание и часто его применяют.

Проще всего делать Drag из тех компонентов, для которых однозначно ясно, что именно перетаскивать. Для этого устанавливаем у источника DragMode = dmAutomatic, а у приемника пишем обработчики событий OnDragOver - разрешение на прием, и OnDragDrop - действия, производимые при окончании перетаскивания.


procedure TForm1.StringGrid2DragOver(Sender, Source: TObject; X,  
  Y: Integer; State: TDragState; var Accept: Boolean);  
begin  
  Accept := Source = Edit1;  
  // разрешено перетаскивание только из Edit1,  
  // при работе программы меняется курсор  
end;  
  
procedure TForm1.StringGrid2DragDrop(Sender, Source: TObject; X,  
  Y: Integer);  
var  
  ACol, ARow: Integer;  
begin  
  StringGrid2.MouseToCell( X, Y, ACol, ARow);  
// находим, над какой ячейкой произвели Drop  
  StringGrid2.Cells[ Acol, Arow] := Edit1.Text;  
//  записываем в нее содержимое Edit1  
end;  

Теперь рассмотрим копирование в упорядоченный список ListBox1 из другого списка. В OnDragOver проверяем, выбран ли хоть один элемент в источнике:


Accept := (Source = ListBox2) and (ListBox2.ItemIndex >= 0);  

В OnDragDrop ищем отмеченные в источнике строки (установлен множественный выбор) и добавляем только те, которых еще нет в приемнике:


for i := 0 to ListBox2.Items.Count - 1 do  
  if (ListBox2.Selected[i]) and (ListBox1.Items.IndexOf(ListBox2.Items[i])<0)  
    then  
      ListBox1.Items.Add(ListBox2.Items[i]);  

Для ListBox2 реализуем перенос строк из ListBox1 и перестановку элементов в желаемом порядке. В OnDragOver разрешаем Drag из любого ListBox:


Accept := (Source is TListBox) and ((Source as TListBox).ItemIndex >= 0);  

А OnDragDrop будет выглядеть так:


var  
  s: string;  
begin  
  if Source = ListBox1 then  
  begin  
    ListBox2.Items.Add(ListBox1.Items[ListBox1.ItemIndex]);  
    ListBox1.Items.Delete(ListBox1.ItemIndex);  
  //удаляем перенесенный элемент  
  end  
  else          //внутренняя перестановка  
  begin  
    s := ListBox2.Items[ListBox2.ItemIndex];  
    ListBox2.Items.Delete(ListBox2.ItemIndex);  
    ListBox2.Items.Insert(ListBox2.ItemAtPos(Point(X, Y), False), s);  
  //находим, в какую позицию переносить и вставляем  
  end;  
end;  

Научимся переносить текст в Memo, вставляя его в нужное место. Поскольку я выбрал в качестве источника любой из ListBox, подключим в Инспекторе Объектов для OnDragOver уже написанный ранее обработчик ListBox2DragOver, а в OnDragDrop напишем


if not CheckBox1.Checked then  // при включении добавляется в конец текста  
begin  
 Memo1.SelStart := LoWord(Memo1.Perform(EM_CHARFROMPOS, 0, MakeLParam(X,Y)));  
    // устанавливаем позицию вставки согласно координатам мыши  
 Memo1.SelText := TListBox(Source).Items[TListBox(Source).ItemIndex];  
end  
  else  
    memo1.lines.add(TListBox(Source).Items[TListBox(Source).ItemIndex]);  

Замечу, что для RichEdit EM_CHARFROMPOS работает несколько иначе, что продемонстрировано в следующем примере. Перенос из Memo реализован с помощью правой кнопки мыши, для того, чтобы не изменять стандартное поведение Memo, и поскольку нажатие левой кнопки снимает выделение. Для Memo1 установлено DragMode = dmManual, а перетаскивание инициируется в OnMouseDown


if (Button = mbRight) and (Memo1.SelLength > 0) then  
    Memo1.BeginDrag(True);  

Обработчик RichEdit1DragOver очевиден, а в RichEdit1DragDrop пишем


var  
  p: tpoint;  
begin  
  if not CheckBox1.Checked then  
  begin  
    p := point(x, y);  
    RichEdit1.SelStart := RichEdit1.Perform(EM_CHARFROMPOS, 0, Integer(@P));  
    RichEdit1.SelText := Memo1.SelText;  
  end  
  else  
    RichEdit1.Lines.Add(Memo1.SelText);  
end;  

Рассмотрим теперь перетаскивание в ListView1 (ViewStyle = vsReport). В OnDragOver разрешим прием из ListBox2 и из себя же:


Accept := ((Source = ListBox2) and (ListBox2.ItemIndex >= 0)) or  
  (Source = Sender);  

А вот OnDragDrop теперь будет посложнее


var  
  Item, CurItem: TListItem;  
begin  
  if Source = ListBox2 then  
  begin  
    Item := ListView1.DropTarget;  
    if Item <> nil then  
    //  случай перетаскивания на Caption  
      if Item.SubItems.Count = 0 then  
        Item.SubItems.Add(ListBox2.Items[ListBox2.ItemIndex])  
    //  добавляем SubItem, если их еще нет  
      else  
        Item.SubItems[0]:=ListBox2.Items[ListBox2.ItemIndex]  
    //  иначе заменяем имеющийся SubItem  
    else  
    begin  
   // при перетаскивании на пустое место создаем новый элемент  
      Item := ListView1.Items.Add;  
      Item.Caption := ListBox2.Items[ListBox2.ItemIndex];  
    end;  
  end  
  
  else // случай внутренней перестановки  
  begin  
    CurItem := ListView1.Selected;  
// запомним выбранный элемент  
    Item := ListView1.GetItemAt(x, y);  
// другой метод определения элемента на который делаем Drop  
    if Item <> nil then  
      Item := ListView1.Items.Insert(Item.Index)  
// вставляем новый элемент перед найденным  
    else  
      Item := ListView1.Items.Add;  
// или добавляем новый элемент в конец  
    Item.Assign(CurItem);  
// копируем исходный в новый  
    CurItem.Free;  
// уничтожаем исходный  
  end;  
end;  

Для ListView2 установим ViewStyle = vsSmallIcon и покажем, как вручную расставлять значки. В OnDragOver зададим условие


Accept := (Sender = Source) and  
    ([htOnLabel,htOnItem, htOnIcon] * ListView2.GetHitTestInfoAt(x, y) = []);   
// пересечение множеств должно быть пустым - запрещаем накладывать элементы  

а код в OnDragDrop очень простой:


ListView2.Selected.SetPosition(Point(X,Y));  

Перетаскивание в TreeView - довольно любопытная тема, здесь порой приходится разрабатывать алгоритмы обхода ветвей для достижения желаемого поведения. Для TreeView1 разрешим перестановку своих узлов в другое положение. В OnDragOver проверим, не происходит ли перетаскивание узла на свой же дочерний во избежание бесконечной рекурсии:


var  
  Node, SelNode: TTreeNode;  
begin  
  Node := TreeView1.GetNodeAt(x, y);  
// находим узел-приемник  
  Accept := (Sender = Source) and (Node <> nil);  
  if not Accept then  
    Exit;  
  SelNode := Treeview1.Selected;  
  while (Node.Parent <> nil) and (Node <> SelNode) do  
  begin  
    Node := Node.Parent;  
    if Node = SelNode then  
      Accept := False;  
  end;  

Код OnDragDrop выглядит так:


var  
  Node, SelNode: TTreeNode;  
begin  
  Node := TreeView1.GetNodeAt(X, Y);  
  if Node = nil then  
    Exit;  
  SelNode := TreeView1.Selected;  
  SelNode.MoveTo(Node, naAddChild);  
// все уже встроено в TreeView  
end;  

Теперь разрешим перенос в TreeView2 из TreeView1


Accept := (Source = TreeView1) and (TreeView2.GetNodeAt(x, y) <> nil);  

И в OnDragDrop копируем выбранную в TreeView1 ветвь во всеми подветвями, для чего придется сделать рекурсивный обход:


var  
  Node: TTreeNode;  
  
  procedure CopyNode(FromNode, ToNode: TTreeNode);  
  var  
    TempNode: TTreeNode;  
    i: integer;  
  begin  
    TempNode := TreeView2.Items.AddChild(ToNode, '');  
    TempNode.Assign(FromNode);  
    for i := 0 to FromNode.Count - 1 do  
      CopyNode(FromNode.Item[i], TempNode);  
  end;  
  
begin  
  Node := TreeView2.GetNodeAt(X, Y);  
  if Node = nil then  
    Exit;  
  CopyNode(TreeView1.Selected, Node);  
end;  

Рассмотрим теперь перенос ячеек в StringGrid1. Поскольку, как и в случае с Memo, простое нажатие левой кнопки занято под другие действия, установим DragMode = dmManual и будем запускать Drag при нажатии левой кнопки, удерживая клавиши Alt или Ctrl. Запишем в OnMouseDown:


var  
  Acol, ARow: Integer;  
begin  
  with StringGrid1 do  
    if (ssAlt in Shift) or (ssCtrl in Shift) then  
    begin  
      MouseToCell(X, Y, Acol, Arow);  
      if (Acol >= FixedCols) and (Arow >= FixedRows) then  
// не будем перетаскивать из фиксированных ячеек  
      begin  
        if ssAlt in Shift then  
          Tag := 1  
        else  
          if ssCtrl in Shift then  
            Tag := 2;  
// запомним что нажато - Alt или Ctrl -  в Tag StringGrid1  
        BeginDrag(True)  
      end  
      else  
        Tag := 0;  
    end;  
end;  

Код OnDragOver учитывает также возможность перетаскивания из StringGrid2 (описание ниже)


var  
  Acol, ARow: Integer;  
begin  
  with StringGrid1 do  
  begin  
    MouseToCell(X, Y, Acol, Arow);  
    Accept := (Acol >= FixedCols) and (Arow >= FixedRows)  
      and (((Source = StringGrid1) and (Tag > 0))  
      or (Source = StringGrid2));  
  end;  
Часть OnDragDrop, относящаяся к внутреннему переносу:


var  
  ACol, ARow, c, r: Integer;  
  GR: TGridRect;  
begin  
  StringGrid1.MouseToCell(X, Y, ACol, ARow);  
  if Source = StringGrid1 then  
    with StringGrid1 do  
    begin  
      Cells[Acol, Arow] := Cells[Col,Row];  
//копируем ячейку-источник в приемник  
      if Tag = 1 then  
        Cells[Col,Row] := '';  
// очищаем источник, если было нажато Alt  
      Tag := 0;  
    end;  

А вот из StringGrid2 сделаем перенос выбранного диапазона ячеек с помощью правой кнопки, для этого в OnMouseDown


if Button = mbRight then  
    StringGrid2.BeginDrag(True);  

И теперь часть StringGrid1DragDrop, относящаяся к переносу из StringGrid2:


if Source = StringGrid2 then  
  begin  
    GR := StringGrid2.Selection;  
// Selection - выделенные в StringGrid2 ячейки  
    for r := 0 to GR.Bottom - GR.Top do  
      for c := 0 to GR.Right - GR.Left do  
        if (ACol + c < StringGrid1.ColCount) and  
          (ARow + r < StringGrid1.RowCount) then  
// застрахуемся от записи вне StringGrid1  
          StringGrid1.Cells[ACol + c, ARow + r] :=  
            StringGrid2.Cells[c + GR.Left, r + GR.Top];  
  end;  

Теперь покажем, как этот диапазон ячеек из StringGrid2 перенести в Memo2. Для этого в OnDragOver Memo2 пишем:


Accept := (Source = StringGrid2) or (Source = DBGrid1);  

и в OnDragDrop Memo2:


var  
  c, r: integer;  
  s: string;  
begin  
  Memo2.Clear;  
  if Source = StringGrid2 then  
    with StringGrid2 do  
      for r := Selection.Top to Selection.Bottom do  
      begin  
        s := '';  
        for c := Selection.Left to Selection.Right do  
          s := s + Cells[c, r] + #9;  
// разделим ячейки табуляцией  
        memo2.lines.add(s);  
      end  

Кроме того, в Memo2 можно переносить выбранную запись из DBGrid1, у которого установлено в Options dgRowSelect = True. В сетке отображается таблица из стандартной поставки Delphi DBDEMOS - Animals.dbf. Перетаскивание осуществляется аналогично StringGrid2, правой кнопкой мыши, только по событию OnMouseMove


if ssRight in Shift then  
    DBGrid1.BeginDrag(true);  

Код в Memo2DragDrop, относящийся к переносу из DBGrid1:


else  
    with DBGrid1.DataSource.DataSet do  
    begin  
      s := '';  
      for c := 0 to FieldCount - 1 do  
        s := s + Fields[c].AsString + ' | ';  
      memo2.lines.add(s);  
    end;  
// в случае dgRowSelect = False для переноса одного поля достаточно сд
елать
// memo2.lines.add(DbGrid1.SelectedField.AsString);
Drag из DBGrid1 принимается также на Panel3, условие приема очевидно, а OnDragDrop выглядит так:


  Panel3.Height := 300;  // раскрываем панель  
  Image1.visible := True;  
  OleContainer1.Visible := false;  
  Image1.Picture.Assign(DBGrid1.DataSource.DataSet.FieldByName('BMP'));  
// показываем графическое поле текущей записи таблицы  

Теперь покажу, как можно передвигать мышью визуальные компоненты в Run-Time. Для Panel1 установим DragMode = dmAutomatic, в OnDragOver формы пишем:


var  
  Ct: TControl;  
begin  
  Ct := ControlAtPos(Point(X + Panel1.Width, Y + Panel1.Height), True, True);  
// для упрощения проверяем перекрытие с другими контролами только правого нижнего угла  
  Accept := (Source = Panel1) and ((Ct = nil) or (Ct = Panel1));  

и в OnDragDrop формы очень просто


Panel1.Left := X;  
Panel1.Top := Y;  

Другой метод перетаскивания можно встретить в каждом FAQ по Delphi:


procedure TForm1.Panel2MouseDown(Sender: TObject; Button: TMouseButton;  
  Shift: TShiftState; X, Y: Integer);  
const  
  SC_DragMove = $F012;  
begin  
  ReleaseCapture;  
  Panel2.Perform(WM_SysCommand, SC_DragMove, 0);  
end;  

И в завершение реализация популярной задачи перетаскивания значков файлов на форму из Проводника. Для этого следует описать обработчик сообщения WM_DROPFILES


private  
 procedure WMDropFiles(var Msg: TWMDropFiles); message WM_DROPFILES;  

В OnCreate формы разрешить прием файлов


DragAcceptFiles(Handle, true);  

и в OnDestroy отключить его


DragAcceptFiles(Handle, False);  

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


procedure TForm1.WMDropFiles(var Msg: TWMDropFiles);  
const  
  maxlen = 254;  
var  
  h: THandle;  
  //i,num:integer;  
  pchr: array[0..maxlen] of char;  
  fname: string;  
begin  
  h := Msg.Drop;  
  
  // дана реализация для одного файла, а   
  //если предполагается принимать группу файлов, то можно добавить:  
  //num:=DragQueryFile(h,Dword(-1),nil,0);  
  //for i:=0 to num-1 do begin  
  //  DragQueryFile(h,i,pchr,maxlen);  
  //...обработка каждого  
  //end;  
  
  DragQueryFile(h, 0, pchr, maxlen);  
  fname := string(pchr);  
  if lowercase(extractfileext(fname)) = '.bmp' then  
  begin  
    Image1.visible := True;  
    OleContainer1.Visible := false;  
    image1.Picture.LoadFromFile(fname);  
    Panel3.Height := 300;  
  end  
  else if lowercase(extractfileext(fname)) = '.doc' then  
  begin  
    Image1.visible := False;  
    OleContainer1.Visible := True;  
    OleContainer1.CreateObjectFromFile(fname, false);  
    Panel3.Height := 300;  
  end  
  else if lowercase(extractfileext(fname)) = '.htm' then  
    ShellExecute(0, nil, pchr, nil, nil, 0)  
  else if lowercase(extractfileext(fname)) = '.txt' then  
    Memo2.Lines.LoadFromFile(fname)  
  else  
    Memo2.Lines.Add(fname);  
  DragFinish(h);  
end;  

При перетаскивании на форму файла с расширением Bmp он отображается в Image1, находящемся на Panel3, Doc загружается в OleContainer, для Htm запускается Internet Explorer или другой браузер по умолчанию, Txt отображается в Memo2, а для остальных файлов в Memo2 будет просто показано имя.

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

В заключение хочу выразить благодарность Игорю Шевченко и Максиму Власову за ценные советы при подготовке примеров.

Добавлено: 07 Августа 2018 07:25:40 Добавил: Андрей Ковальчук

Создание калькулятора с командной строкой в Delphi

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

Для начала предлагаю немного теории.

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

Для начала представим себе простейший вариант строки:

1*2+3*4

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

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

Далее Вы спросите: «А зачем, собственно, мы это делали?». Так просто очень легко считать.

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

Ищем во втором списке операции, приоритет которых выше. То есть, знаки умножения или деления.
Если нашли, то вынимаем этот знак. Вынимаем из первого списка число с таким же номером, как и у знака, и следующее. Это и будут наши операнды. Выполняем с ними соответствующее действие, и записываем результат в первый список на то место, с которого выдернули первый операнд.
Повторяем пункты 1 и 2 до тех пор, пока во втором списке не останется ни одного такого знака.
Повторяем пункты 1-3 со знаками сложения и вычитания.
А теперь давайте прогоним через этот алгоритм наш пример.

Ищем в правом столбце знак умножения или деления. Нашли – он стоит на первой позиции. Далее нам нужны числа, которые надо умножить. Они стоят во втором списке под номерами 1, и 1+1, то есть 2. В нашем примере это цифры 1 и 2. Вынимаем их из списка и умножаем. Получилась двойка. Записываем её в первый список на то место, откуда выдернули первый операнд, в нашем случае на первое место. И удаляем из второго списка знак умножения. Вот что должно получиться:

2.Далее продолжаем искать знаки умножения или деления. Нашли. Знак умножения стоит на второй позиции. Нам опять же нужны операнды. Они находятся на втором и третьем месте в первом списке. Это цифры 3 и 4. Умножаем их и удаляем из первого списка. Результат, число 12, заносим в первый список под номером 2.

На этом знаки умножения и деления у нас закончились, займемся поисками знаков сложения и вычитания. В нашем случае есть знак умножения, стоящий на первом месте. Выбираем из первого списка то, что будем складывать (это элементы под номерами 1 и два), удаляем их из списка, а результат сложения заносим обратно в список. У нас получилось следующее:



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


function CalculateLists (s1, s2: TStringList): real;  
var  
    i: integer;  
    a,b,r1: real;  
    c: char;  
begin  
 r1 := 0;  
// Ищем знаки  умножения или деления  
i := 0;  
if s2.Count>0 then  
while (s2.Find('*', i)or(s2.Find('/', i))) do  
begin  
  c := s2[i][1];  
  a := strtofloat(s1[i]);  
  b := strtofloat(s1[i+1]);  
  case c of  
   '*': r1 := a*b;  
   '/': r1 := a/b;  
  end;  
  s1.Delete(i);  
  s1.Delete(i);  
  s1.Insert(i, floattostr(r1));  
  s2.Delete(i);  
end;  
// Сложение и вычитание ///  
if s2.Count>0 then  
repeat  
    c := s2[0][1];  
    a := strtofloat(s1[0]);  
    b := strtofloat(s1[1]);  
    case c of  
     '+': r1 := a+b;  
     '-': r1 := a-b;  
    end;  
    s1.Delete(0);  
    s1.Delete(0);  
    s1.Insert(0, floattostr(r1));  
    s2.Delete(0);  
    if s1.Count = 1 then break;  
    if s2.Count = 0 then break;  
until false;  
 result := strtofloat(s1[0]);  
end;  

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

Ну а теперь давайте займемся самой сложной частью всего задания: разделение строки на два списка. Давайте, прежде чем включать Delphi и начинать лупить клавиатуру, немного разберемся, чего мы от этой функции хотим. Главная её задача состоит в том, чтобы корректно разделить строку на два списка и передать эти списки на вычисление функции CalculateLists, которую мы только что написали. А что, если мы наткнемся на неверный символ? Для того, чтобы в Вашей основной программе Вы смогли верно определить какая произошла ошибка и на каком символе, я предлагаю создать свой класс-исключение. И возбуждать это исключение при каждой ошибке обработки строки. Этот класс самый простой, просто чтобы не загромождать проект. Вы можете изменить его по Вашему желанию.


type ECalcError = class (Exception)  
end;  

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


const Sign: set of char = ['+', '-', '*', '/'];  
var Digits: set of char = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];  
type TFunc = function (x: real): real;  
const MaxFunctionID = 2; // - Количество обрабатываемых функций  

Так как использовать ссылки на стандартные процедуры нельзя, или я просто не знаю как. Поэтому нам придется переопределить парочку функций.


function _sin(x: real):real;  
begin  
 result := sin(x);  
end;  
function _cos(x: real):real;  
begin  
 result := cos(x);  
end;  

А теперь можно и определять два массива функций:


const sfunc: array [1..MaxFunctionID] of string[7]= ('sin', 'cos');  
const ffunc: array [1..MaxFunctionID] of TFunc = (_sin, _cos);  

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

Для нахождения функции
Для подсчета значения функции.
Напишем функцию, для проверки, есть ли в строке поддерживаемая функция. Я считаю, что она довольно простая, поэтому сразу приведу её код:


function GetFunction (Line: string; index: integer): integer;  
var i: integer;  
begin  
 for i:= 1 to MaxFunctionID do  
  if sfunc[i] = copy(Line, index, length(sfunc[i])) then  
   begin  
    result := i;  
    exit;  
   end;  
 result := 0;  
end;  

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

Функция для подсчета значения выглядит еще проще:


function CalculateFunction (Fid: integer; x: real): real;  
begin  
 result := ffunc[Fid](x);  
end;  

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


function GetFunctionNameLength (Fid: integer): integer;  
begin  
 result := length (sfunc[Fid]);  
end;  

Вот мы и закончили писать подготовительные функции. Давайте разберем примерный алгоритм функции разбора строки. Мы будем просматривать строку по символам. Если очередной символ есть цифра, то заносим эту цифру в строку-число. Если символ – знак операции, то записываем в список операций эту операцию, и записываем строку-число в список чисел. Если нам попалась открывающая скобка, то мы должны найти её закрывающую, независимо есть ли в этих скобках вложенные скобки и передать строку, которая находиться в этих скобках функции разделения строк, и записать в массив чисел то, что она вернет. Этот пример называется рекурсией. Идем дальше, если мы нашли символ, не являющийся скобкой, цифрой или знаком операции. Это должно быть функция. Вот здесь нам и пригодится функция для проверки, начинается ли с этой позиции поддерживаемая функция. Если да, то ищем после записи этой функции открывающую скобку, так как параметр любой функции должен идти в скобках. Если скобки нет, то можно вызвать ошибку. Если скобка есть, то действуем по намеченному алгоритму обработки скобки, только в список чисел надо вносить не результат выполнения функции разбиения, а значение функции с этим результатом.

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


function Calculate (Line1: string): real;  
var z, d: TStringList;            // - z –список знаков; d – список чисел  
    i, j, c: integer;                //  счетчики  
    w, l, Line: string;                     // begw – переменная, отвечающая за начало числа  
    begw, ok: boolean;  
    res: real;                       // - результат  
    e: ECalcError;                // - ошибка  
    id : integer;                    // - номер функции  
begin  
 w := '';  
 Line := Line1;  
 begw := FALSE;  
 ok := false;  
 z := TStringList.Create;  
 d := TStringList.Create;  
//// Разбиение строки на два списка  ////  
 i := 1;  
 repeat  
 ////  Если знак операции ////  
  if Line[i] in Sign then  
   begin  
    z.Add(Line[i]);  
    if begw then d.Add(w);  
    w := '';  
    begw := TRUE;  
   end  
 ////  Если цифра ////  
  else if Line[i] in digits then  
   begin  
    begw := true;  
    w := w + Line[i];  
   end  
 //// Если скобка ////  
  else if Line[i]='(' then  
   begin  
    c := 1;  
    for j := i+1 to length (Line) do  
     begin  
      if Line[j]='(' then c := c + 1;  
     if Line[j]=')' then c := c - 1;  
      if c=0 then  
       begin  
        ok := true;  
        break;  
       end;  
     end;  
    if not ok then  
     begin  
      e := ECalcError.Create('Не найдена закрывающая скобка к символу ' + inttostr(i));  
      raise e;  
      e.Free;  
     end;  
    l := copy (Line, i+1, j-i-1);  
    d.Add(floattostr(Calculate(l)));  
    delete (Line, i, j-i+1);  
    i := i - 1;  
   end  
 /// Проверка на функцию  
  else if (GetFunction (Line, i)<>0) then  
   begin  
    id := GetFunction (Line, i);  
    if Line[i+GetFunctionNameLength(id)]<>'(' then  {Если после функции нет скобки}  
     begin  
      e := ECalcError.Create('Не найдена скобка после функции в символе  '+ inttostr(i));  
      raise e;  
      e.Free;  
     end;  
{----Если есть скобка----------}  
    c := 1;  
    for j := i+GetFunctionNameLength(id)+1 to length (Line) do  
     begin  
      if Line[j]='(' then c := c + 1;  
      if Line[j]=')' then c := c - 1;  
      if c=0 then  
       begin  
        ok := true;  
        break;  
       end;  
     end;  
    if not ok then  
     begin  
      e := ECalcError.Create('Не найдена закрывающая скобка к символу' + inttostr(i));  
      raise e;  
      e.Free;  
     end;  
    l := copy (Line, i+GetFunctionNameLength(id)+1, j-i-GetFunctionNameLength(id)-1);  
    d.Add(floattostr(CalculateFunction(id, Calculate(l))));  
    delete (Line, i, j-i+1);  
    i := i - 1;  
   end  
 ////  Если неизвестный символ ////  
  else  
   begin  
    e := ECalcError.Create('Неизвестный символ : '+inttostr(i));  
    raise e;  
    e.Free;  
   end;  
  i := i + 1;  
  j := Length (Line);  
  if i>J then break;  
 until false;  
 if w<>'' then d.Add(w);  
 res := (CalculateLists(d, z));  
 z.Free;  
 d.Free;  
 result := res;  
end;  

Вот и все. Надеюсь, Вы быстро разобрались в этой функции.

Добавлено: 07 Августа 2018 07:22:34 Добавил: Андрей Ковальчук