![]() |
Упростите свои Delphi-приложения - Части 3 и 4Источник: deviabe
Теперь у нас есть идея - то, чего мы хотим достичь и как мы собираемся это сделать, время написать код и спроектировать классы. 1. ВведениеВ качестве основы нам потребуется класс/объект, который мы сможем использовать для чтения и записи настроек приложения из и в реестр Windows. Звучит вполне просто... но, как вы помните, мы подумали предусмотреть расширение функциональности в дальнейшем.
2. Требования к коду2.1. Совместимость с Delphi 7Хотя в последние годы язык пополнился некоторыми новыми элементами, мы же пока использовать их не будем. Наша цель - компиляция кода в Delphi 7. Вы можете задаться вопросом: "Кто еще работает в этой старой Delphi?". Я заметил, что даже сегодня, некоторые мои клиенты используют Delphi 7 для компиляции своих проектов. В дальнейших статьях я может быть покажу Вам, как сделать то, что мы делаем, с использованием современных методов, но сейчас давайте остановимся на этом.
2.2. Отсутствие "привязки" к реестру WindowsНесмотря на то, что мы будем писать код для чтения и записи данных в реестр Windows, хочется легко адаптировать его для других хранилищ, например XML или INI-файла. Да и кто знает, что будет завтра. Не исключено, что мы получим возможность писать приложения для Windows Mobile, Mac, iPhone или даже iPad (было бы неплохо), и реестра Windows на этих платформах может не оказаться. Сейчас сфокусируемся на реестре Windows, но, как Вы уже поняли, иметь ввиду другие хранилища - хорошая идея. Главное, о чем должна болеть наша голова в данный момент - возможность хранения/загрузки настроек. Как или где они будут хранится не так важно, сделаем то, что требуется.
2.3. Еще ньюансыПока мы знаем, что нам нужно что-то, что сможет хранить наши настройки. Нам понадобится загружать и сохранять их, а также, возможно, наличие имени для каждой настройки или даже значения по умолчанию или ее описание. Мы будем хранить Целые числа, Строки, а может быть и Пароли, Даты, ...
3. Время кодинга!3.1. ... ну почти ...Так, ... вообще, до того, как начать писать код, стоит посмотреть, как подобные вещи реализованы в VCL. Конечно, мы все можем сделать и сами, но позволить новым классам наследоваться от существующих было бы неплохой идеей. Т.к. нам нужен список, Вы можете взять, например, класс TList. В моем случае, я знал, что хочу иметь настройки, в которых будут храниться строки, целые числа и булевы значения. После я решил, что нужны настройки и для хранения значений типа DateTime, а также некоторых других типов. В конце концов я пришел к чему-то похожему на TField и TIntegerField, TStringField, ... Итак, зная, что я буду использовать различные типы данных в настройках и хочу хранить список этих настроек, я решил, что неплохо было бы подключить к работе TObjectList.
3.2. Создание класса TdvSetting3.2.1. ПреамбулаВ общем случае, мне нужен объект со следующими свойствами:
Необходимо иметь возможность чтения и записи значения конкретного идентификатора в реестр. Кроме того, при чтении значения, я хочу проверять, если ли уже значение у данного идентификатора, а в случае его отсутствия использовать значение по умолчанию. Я хотел бы получать значения TdvSetting в виде String или Variant (подобно TField и TStringField), поэтому реализовал эту возможность в коде. Плюс, я хочу устанавливать значение TdvSetting. И наконец, как и с TField в VCL, я добавил код, вызывающий исключение, если потомок не реализует какой-либо метод. Это может показаться несколько сложным, но давайте сравним TdvSetting с TField и TStringField еще раз. С TStringField Вы можете присвоить значение, используя aField.Value := theValue или aField.AsString := theValue Оба варианта присваивания верны, но, если aField - экземпляр TField, а не TStringField - возникнет исключение. Ту же функциональность сделал и я. Сейчас я сосредоточусь на реестре Windows, но как Вы знаете неплохо было бы иметь и другие возможности хранения. В конце концов, единственная вещь, на которой мы сейчас акцентируем внимание - хранение/загрузка некоторых настроек. Как или где они будут храниться не так важно, сначала сделаем то, что хотели.
3.2.2. КодTdvSetting = class(TObject)
private
FValue: Variant;
FDefaultValue: Variant;
FIdentifier: string;
FCaption: string;
procedure SetCaption(const Value: string);
procedure SetVisible(const Value: Boolean);
protected
function GetAsBoolean: Boolean; virtual;
function GetAsDateTime: TDateTime; virtual;
function GetAsFloat: Double; virtual;
function GetAsInteger: Longint; virtual;
function GetAsString: string; virtual;
function GetAsVariant: Variant; virtual;
procedure SetAsBoolean(const Value: Boolean); virtual;
procedure SetAsDateTime(const Value: TDateTime); virtual;
procedure SetHint(const Value: string);
procedure SetAsFloat(const Value: Double); virtual;
procedure SetIdentifier(const Value: string);
procedure SetAsInteger(const Value: Longint); virtual;
procedure SetAsString(const Value: string); virtual;
procedure SetAsVariant(const Value: Variant); virtual;
protected
function AccessError(const TypeName: string): Exception; dynamic;
procedure SetVarValue(const Value: Variant); virtual;
public
constructor Create(const aIdentifier, aCaption: string;
const aDefaultValue: Variant); virtual;
destructor Destroy; override;
procedure SaveToRegIni(aRegIni: TRegistryIniFile;
const aSection: string); virtual;
procedure LoadFromRegIni(aRegIni: TRegistryIniFile;
const aSection: string); virtual;
procedure Clear; virtual;
property DefaultValue: Variant read FDefaultValue;
property AsBoolean: Boolean read GetAsBoolean write SetAsBoolean;
property AsDateTime: TDateTime read GetAsDateTime write SetAsDateTime;
property AsFloat: Double read GetAsFloat write SetAsFloat;
property AsInteger: Longint read GetAsInteger write SetAsInteger;
property AsString: string read GetAsString write SetAsString;
property AsVariant: Variant read GetAsVariant write SetAsVariant;
property Identifier: string read FIdentifier write SetIdentifier;
property Caption: string read FCaption write SetCaption;
property Value: Variant read GetAsVariant write SetAsVariant;
end;
...
function TdvSetting.AccessError(const TypeName: string): Exception;
resourcestring
SSettingAccessError = 'Невозможно получить значение ''%s'' (%s) как %s';
begin
Result := Exception.CreateResFmt(@SSettingAccessError,
[ Identifier, Caption, TypeName ]);
end;
procedure TdvSetting.Clear;
begin
FValue := Null;
end;
constructor TdvSetting.Create(const aIdentifier, aCaption: string;
const aDefaultValue: Variant);
begin
Create(aIdentifier, aCaption, aCaption, True, aDefaultValue);
end;
function TdvSetting.GetAsBoolean: Boolean;
begin
raise AccessError('Boolean'); { Do not localize }
end;
function TdvSetting.GetAsDateTime: TDateTime;
begin
raise AccessError('DateTime'); { Do not localize }
end;
function TdvSetting.GetAsFloat: Double;
begin
raise AccessError('Float'); { Do not localize }
end;
function TdvSetting.GetAsInteger: Longint;
begin
raise AccessError('Integer'); { Do not localize }
end;
function TdvSetting.GetAsString: string;
begin
Result := ClassName;
end;
function TdvSetting.GetAsVariant: Variant;
begin
raise AccessError('Variant'); { Do not localize }
end;
procedure TdvSetting.LoadFromRegIni(aRegIni: TRegistryIniFile;
const aSection: string);
begin
Assert(Assigned(aRegIni), 'Параметр aRegIni должен содержать экземпляр TRegIni');
end;
procedure TdvSetting.SaveToRegIni(aRegIni: TRegistryIniFile;
const aSection: string);
begin
Assert(Assigned(aRegIni), 'Параметр aRegIni должен содержать экземпляр TRegIni');
end;
procedure TdvSetting.SetAsBoolean(const Value: Boolean);
begin
raise AccessError('Boolean'); { Do not localize }
end;
procedure TdvSetting.SetAsDateTime(const Value: TDateTime);
begin
raise AccessError('DateTime'); { Do not localize }
end;
procedure TdvSetting.SetAsFloat(const Value: Double);
begin
raise AccessError('Float'); { Do not localize }
end;
procedure TdvSetting.SetAsInteger(const Value: Longint);
begin
raise AccessError('Integer'); { Do not localize }
end;
procedure TdvSetting.SetAsString(const Value: string);
begin
raise AccessError('string'); { Do not localize }
end;
procedure TdvSetting.SetAsVariant(const Value: Variant);
begin
if (VarIsNull(Value)) then
begin
Clear;
end
else
begin
SetVarValue(Value);
end;
end;
procedure TdvSetting.SetCaption(const Value: string);
begin
FCaption := Value;
end;
procedure TdvSetting.SetHint(const Value: string);
begin
FHint := Value;
end;
procedure TdvSetting.SetIdentifier(const Value: string);
begin
FIdentifier := Value;
end;
procedure TdvSetting.SetVarValue(const Value: Variant);
begin
raise AccessError('Variant'); { Do not localize }
end;
3.2.3. Что он делает?На самом деле, этот кусок кода практически ничего не делает. Он просто приводит создание базового класса, которые мы сможем использовать в качестве родительского для остальных классов. Попросту, мы имеет некоторую обработку ошибок и скелет для наших классов настроек.
3.3. Класс TdvStringSetting3.3.1. ПреамбулаВ общем, класс TdvSetting предоставляет нам скелет, который мы можем использовать для создания настроек-потомков. Я уже завершил написание TdvStringSetting, TdvIntegerSetting, TdvBooleanSetting и некоторых других, но давайте начнем с TdvStringSetting.
3.3.2. КодTdvStringSetting = class(TdvSetting)
private
function GetDefaultValueAsString: string;
protected
function GetAsBoolean: Boolean; override;
function GetAsDateTime: TDateTime; override;
function GetAsFloat: Double; override;
function GetAsInteger: Longint; override;
function GetAsString: string; override;
function GetAsVariant: Variant; override;
function GetValue(var Value: string): Boolean;
procedure SetAsBoolean(const Value: Boolean); override;
procedure SetAsDateTime(const Value: TDateTime); override;
procedure SetAsFloat(const Value: Double); override;
procedure SetAsInteger(const Value: Longint); override;
procedure SetAsString(const aValue: string); override;
procedure SetVarValue(const aValue: Variant); override;
public
procedure SaveToRegIni(aRegIni: TRegistryIniFile; const aSection: string); override;
procedure LoadFromRegIni(aRegIni: TRegistryIniFile; const aSection: string); override;
property DefaultValue: string read GetDefaultValueAsString;
property Value: string read GetAsString write SetAsString;
end;
...
{ TdvStringSetting }
function TdvStringSetting.GetAsBoolean: Boolean;
var
S: string;
begin
S := GetAsString;
Result := (Length(S) > 0) and (S[1] in ['T', 't', 'Y', 'y']);
end;
function TdvStringSetting.GetAsDateTime: TDateTime;
begin
Result := StrToDateTime(GetAsString);
end;
function TdvStringSetting.GetAsFloat: Double;
begin
Result := StrToFloat(GetAsString);
end;
function TdvStringSetting.GetAsInteger: Longint;
begin
Result := StrToInt(GetAsString);
end;
function TdvStringSetting.GetAsString: string;
begin
if not GetValue(Result) then Result := '';
end;
function TdvStringSetting.GetAsVariant: Variant;
var
S: string;
begin
if GetValue(S) then Result := S else Result := Null;
end;
function TdvStringSetting.GetDefaultValueAsString: string;
begin
Result := FDefaultValue;
end;
function TdvStringSetting.GetValue(var Value: string): Boolean;
begin
Value := FValue;
Result := True;
end;
procedure TdvStringSetting.LoadFromRegIni(aRegIni: TRegistryIniFile;
const aSection: string);
begin
inherited LoadFromRegIni(aRegIni, aSection);
Value := aRegIni.ReadString(aSection, Identifier, DefaultValue);
end;
procedure TdvStringSetting.SaveToRegIni(aRegIni: TRegistryIniFile;
const aSection: string);
begin
inherited SaveToRegIni(aRegIni, aSection);
aRegIni.WriteString(aSection, Identifier, Value);
end;
procedure TdvStringSetting.SetAsBoolean(const Value: Boolean);
const
Values: array[Boolean] of string[1] = ('F', 'T');
begin
SetAsString(Values[Value]);
end;
procedure TdvStringSetting.SetAsDateTime(const Value: TDateTime);
begin
SetAsString(DateTimeToStr(Value));
end;
procedure TdvStringSetting.SetAsFloat(const Value: Double);
begin
SetAsString(FloatToStr(Value));
end;
procedure TdvStringSetting.SetAsInteger(const Value: Integer);
begin
SetAsString(IntToStr(Value));
end;
procedure TdvStringSetting.SetAsString(const aValue: string);
begin
FValue := aValue;
end;
procedure TdvStringSetting.SetVarValue(const aValue: Variant);
begin
SetAsString(aValue);
end;
3.3.3. Что он делает?Т.к. у нас уже имеется скелет от TdvSetting, нам остается лишь переопределить некоторые методы и реализовать свои собственные функции. Как видите, мы добавили примерно такой же код, который имеет TStringField в VCL. Дополнительно реализованы лишь методы SaveToRegIni и LoadFromRegIni. Они позволяют загрузить и сохранить значение настройки в реестр. Кроме того, при загрузке мы используем значение по умолчанию, если настройка не найдена в реестре. Секция реестра, откуда/куда мы будем загружать/сохранять значение, имеет такое же имя, как и идентификатор настройки.
3.4. Создание класса TdvSetting3.4.1. ПреамбулаТеперь, когда мы спроектировали различные типы настроек, нам понадобится какой-либо контейнер для их хранения. Например, приложение может иметь несколько настроек: PrintInColor (Цветная печать), CheckForUpdates (Проверять обновления), AutoConnect (Автоподключение), ..., и мы должны иметь к ним доступ. Как уже было сказано ранее, я создал TdvSettings на основе TObjectList. Так мы сможем хранить ссылку на экземпляр каждого TdvSetting-объекта и иметь к ним доступ.
3.4.2. КодTdvSettings = class(TObjectList)
private
FRootKey: string;
protected
procedure CreateSettings; virtual;
function GetItems(Index: Integer): TdvSetting;
procedure SetItems(Index: Integer; ASetting: TdvSetting);
public
constructor Create(const aRootKey : string);
function Add(ASetting: TdvSetting): Integer;
function Extract(Item: TdvSetting): TdvSetting;
function Remove(ASetting: TdvSetting): Integer;
function IndexOf(ASetting: TdvSetting): Integer;
function First: TdvSetting;
function Last: TdvSetting;
function SettingByIdentifier(const aIdentifier : string) : TdvSetting;
procedure LoadFromRegistry;
procedure SaveToRegistry;
procedure Insert(Index: Integer; ASetting: TdvSetting);
property Items[Index: Integer]: TdvSetting read GetItems write SetItems; default;
property RootKey : string read FRootKey write FRootKey;
end;
...
{ TdvSettings }
function TdvSettings.Add(ASetting: TdvSetting): Integer;
begin
Result := inherited Add(ASetting);
end;
constructor TdvSettings.Create(const aRootKey: string);
begin
inherited Create(True);
FRootKey := aRootKey;
CreateSettings;
// Читаем значения из реестра при создании списка
LoadFromRegistry;
end;
procedure TdvSettings.CreateSettings;
begin
end;
function TdvSettings.Extract(Item: TdvSetting): TdvSetting;
begin
Result := TdvSetting(inherited Extract(Item));
end;
function TdvSettings.First: TdvSetting;
begin
Result := TdvSetting(inherited First);
end;
function TdvSettings.GetItems(Index: Integer): TdvSetting;
begin
Result := TdvSetting(inherited Items[Index]);
end;
function TdvSettings.IndexOf(ASetting: TdvSetting): Integer;
begin
Result := inherited IndexOf(aSetting);
end;
procedure TdvSettings.Insert(Index: Integer; ASetting: TdvSetting);
begin
inherited Insert(Index, aSetting);
end;
function TdvSettings.Last: TdvSetting;
begin
Result := TdvSetting(inherited Last);
end;
procedure TdvSettings.LoadFromRegistry;
var
lIndex: Integer;
lSetting: TdvSetting;
lRegIni: TRegistryIniFile;
begin
lRegIni := TRegistryIniFile.Create('');
try
for lIndex := 0 to Pred(Count) do
begin
lSetting := Items[lIndex];
lSetting.LoadFromRegIni(lRegIni, RootKey);
end;
finally
FreeAndNil(lRegIni);
end;
end;
function TdvSettings.Remove(ASetting: TdvSetting): Integer;
begin
Result := inherited Remove(aSetting);
end;
procedure TdvSettings.SaveToRegistry;
var
lIndex: Integer;
lSetting: TdvSetting;
lRegIni: TRegistryIniFile;
begin
lRegIni := TRegistryIniFile.Create('');
try
for lIndex := 0 to Pred(Count) do
begin
lSetting := Items[lIndex];
lSetting.SaveToRegIni(lRegIni, RootKey);
end;
finally
FreeAndNil(lRegIni);
end;
end;
procedure TdvSettings.SetItems(Index: Integer; ASetting: TdvSetting);
begin
inherited Items[Index] := aSetting;
end;
function TdvSettings.SettingByIdentifier(
const aIdentifier: string): TdvSetting;
var
lcv: Integer;
begin
Result := Nil;
for lcv := 0 to Pred(Count) do
begin
if (Items[lcv].Identifier = aIdentifier) then
begin
Result := Items[lcv];
Break;
end;
end;
end;
3.4.3. Что он делает?Итак, это простой контейнер для нескольких объектов класса TdvSetting. Он предоставляет доступ к каждой настройке по ее индексу в списке или идентификатору (имени). Мы также сможем устанавливать корневой узел (RootKey) TdvSettings-объекта, задающий общий путь в реестре ко всем настройкам. Когда-нибудь мне захочется иметь возможность создавать настройки из базового класса и загружать их значения. Вы заметите, что я добавил пустой метод CreateSettings. Цель - реализовать его в дочерних классах. На уровне TdvSettings мы не знаем, какие настройки у нас есть, как они называются и какой тип они имеют... В будущем же я хочу иметь возможность создавать настройки из базового класса и загружать их значения. В TdvMyApplicationSettings я переопределю этот метод и добавлю необходимый код для настройки конкретных TdvSetting-объектов, которые мне нужны в приложении.
4. Что дальше?На данный момент у нас есть скелет, от которого мы можем оттолкнуться. Если Вы готовы повозиться, можете создать свой собственный TdvSetting и его потомков. Я уже говорил, что мне нужны были Integer, DataTime и Boolean - их и попробуйте реализовать. Я уже неоднократно отмечал, что не существует единственного подхода к решению нашей проблемы, и старался привести и другие возможные способы. Это позволило мне прийти к тому, что есть сейчас. Например, начальные версии содержали код для TRegistryIniFile непосредственно внутри класса TdvSetting. Наличие одного и того же кода для LoadFromRegIni и SaveToRegIni во всех потомках TdvSetting, заставило меня в скором времени призадуматься. Я решил, что не стоит создавать и уничтожать TRegistryIniFile для каждой из 20 настроек - так код пришел к своему текущему состоянию. |