Спростіть свої Delphi-програми – Частини 3 і 4, Різне, Програмування, статті

Тепер у нас є ідея – те, чого ми хочемо досягти і як ми збираємося це зробити, час написати код та спроектувати класи.

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. Створення класу TdvSetting


3.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. Клас TdvStringSetting


3.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. Створення класу TdvSetting


3.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 налаштувань – так код прийшов до свого поточному стану.


Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*