|
|
|||||||||||||||||||||||||||||
|
Упростите свои 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 настроек - так код пришел к своему текущему состоянию.
|
|