INIциализация параметров приложения или частное решение вопроса визуализации настроек приложения

© 2003 Булуй Юрий
bouloui@tii.ru

Введение в проблему.

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

Обычно, для того чтобы пользователь мог управлять Настройками, в приложении создается отдельная форма (форма Настроек приложения), где каждой конкретной Настройке сопоставляется тот или иной визуальный элемент управления. Таким образом, возникает задача "связки" элементов управления (пользовательского интерфейса) с Настройками. Конечно же, такую задачу можно решать каждый раз, с каждым новым приложением заново, используя разные подходы. Но почему бы, не создать (один раз написав код) визуальный компонент (на основе существующих), который бы был "информирован" о параметре, и умел бы считывать его значение, и сохранять его после изменения? К тому же идеология Delphi, как представителя RAD систем, предоставляет нам такую возможность. Попытаемся решить эту задачу.

Понимание контекста

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

  1. Каким образом будут храниться Настройки?
  2. Какие стандартные визуальные компоненты могут быть взяты за основу для визуализации конкретной Настройки, в зависимости от ее природы?
  3. Какова стратегия управления чтением/записью значения Настройки в форме Настроек?

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

Отвечая на второй вопрос, можно утверждать, что в зависимости от природы конкретной Настройки, для удобства пользователя, применяются разные визуальные компоненты. Так, для Настроек имеющих только два возможных значения (например, True/False) может быть использован (в зависимости от контекста) компонент на основе TCheckBox. А для выбора из ограниченного набора значений - компонент на основе TComboBox. Для простых случаев мы можем создать компонент на основе TLabel или TLabeledEdit. Этот набор компонент покрывает большую часть потребностей, поэтому далее мы будем использовать в качестве основы именно эти компоненты.

Эти два вопроса тесно связаны между собой. Так, сразу же возникает проблема формата хранения в INI-файле списка выбора и текущего значения при использовании компонента TComboBox, в случае, когда природа Настройки требует выбора из ограниченного набора значений. Одно из возможных решений - хранить в INI-файле, не только текущее значение данной Настройки, но и весь список выбора. Избегая описания более сложных случаев, требующих мэппинга между удобным для пользователя представлением информации о значении данной Настройки и внутренним значением настройки (например, определяя отношение IsMappedTo('ntInteger', 'Целое число')), остановимся на более простом случае описания:

[DebetPkg]
GUIType=Плоский|Нормальный|Ультра Плоский.

Данный пример хранения показывает возможность выбора из трех вариантов значения Настройки GUIType (тип отображения GUI) { Плоский, Нормальный, Ультра Плоский }. Примем соглашение, что текущим значением у Настроек такой природы является то, которое стоит первым в списке (в нашем примере - Плоский). В целях отделения вариантов выбора друг от друга будем использовать символ "|".

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

Определение интерфейса для работы с INI-файлом

Сформулировав задачу и осознав ее контекст, можно сделать дальнейший шаг к реализации. Для начала определим необходимый набор методов и свойств, который позволит реализовать "информированность" визуального компонента о конкретной Настройке или другими словами, реализация которых позволит осуществить чтение/запись из INI-файла:

procedure SaveValToIni();
procedure LoadValFromIni();
property IniFileObj : TIniFile read GetIniFile write SetIniFile;
property IniFileName : string read GetIniFileName write SetIniFileName;
property SectionName : string read GetSectionName write SetSectionName;
property KeyName : string read GetKeyName write SetKeyName;

Теперь более подробно остановимся на приведенном выше интерфейсе класса. Мы определили ряд свойств, которые необходимы, для того, чтобы получить/записать значения Настройки. Если SectionName и KeyName не вызывают никаких вопросов, то IniFileObj и IniFileName требуют некоторого пояснения. Практически это означает, что мы для получения доступа к чтению/записи из/в INI-файл собираемся использовать класс TIniFile, и эти два свойства определяют метод получения объекта этого класса - либо сославшись на уже созданный в приложении объект класса TIniFile, либо, в случае отсутствия такого объекта - инициировать его создание в теле методов самого компонента, на основании указанного в IniFileName имени INI-файла. Методы SaveValToIni() и LoadValFromIni() призваны служить для сохранения и чтения значения Настройки из ini-файла. Следует отметить, что данный интерфейс должен быть размещен в секцию published, чтобы можно было работать с этими свойствами в design-time.

Теперь, мы наконец-то можем подойти вплотную к вопросу реализации.

TIniCheckBox

Для начала напишем реализацию компонента-потомка от TСheckBox -- TIniCheckBox. Самой простой реализацией такого компонента, было бы, объявить новый класс TiniCheckBox как:

TIniCheckBox = class(TCheckBox)
тем самым декларируя, что мы добавляем дополнительную функциональность к компоненту TCheckBox. И далее добавить в TIniCheckBox свойства и методы описанного выше интерфейса которые обеспечивают необходимую нам функциональность. Но такое решение, при использовании компонентов в форме настроек, потребовало бы явного перечисления имен компонентов или проверки на наличие определенных свойств у компонента (например IniFileObj и IniFileName) или принадлежности к определенному типу с приведением к нему. Но это не позволит нам выполнить требования, описанные в п. 3 раздела "Понимание контекста" о желании иметь программный код, не требующий внесения изменений в код для осуществления чтения/записи значений при добавлении новых Настроек! Да, нам явно не хватает абстрактного класса, декларирующего наш интерфейс, приводя к которому можно было бы унифицированно обращаться к методам SaveValToIni() и LoadValFromIni(). Одно из возможных решений - наследование не от конечного класса TСheckBox, а от TСustomCheckBox, или вообще от общего предка визуальных компонент..., а можно сделать несколько иначе. На роль абстрактного класса вполне подходит конструкция языка Delphi - interface. Тогда, в терминах этой конструкции, мы можем объявить:
IIniOperations = interface
   ['{95EFA38B-2712-4DD4-A7B0-21CD7C30CFFB}']
   function GetIniFile() : TIniFile;
   procedure SetIniFile(const Value : TIniFile);
   function GetIniFileName() : string;
   procedure SetIniFileName(const Value : string);
   function GetSectionName() : string;
   procedure SetSectionName(const Value : string);
   function GetKeyName() : string;
   procedure SetKeyName(const Value : string);
   procedure SaveValToIni();
   procedure LoadValFromIni();
   property IniFileObj : TIniFile read GetIniFile write SetIniFile;
   property IniFileName : string read GetIniFileName write SetIniFileName;
   property SectionName : string read GetSectionName write SetSectionName;
   property KeyName : string read GetKeyName write SetKeyName;
end;
и "попросим" наш класс реализовать этот интерфейс:
TIniCheckBox = class(TCheckBox, IIniOperations)
private
   FSectionName: string;
   FIniFileName: string;
   FKeyName: string;
   FIniObjAssigned : boolean;
   FIniFileObj : TIniFile;
   function GetIniFile() : TIniFile;
   procedure SetIniFile(const Value : TIniFile);
   function GetIniFileName() : string;
   procedure SetIniFileName(const Value : string);
   function GetSectionName() : string;
   procedure SetSectionName(const Value : string);
   function GetKeyName() : string;
   procedure SetKeyName(const Value : string);
public
   { Public declarations }
   procedure SaveValToIni();
   procedure LoadValFromIni();
   property IniFileObj : TIniFile read GetIniFile write SetIniFile;
published
   { Published declarations }
   property IniFileName : string read GetIniFileName write SetIniFileName;
   property SectionName : string read GetSectionName write SetSectionName;
   property KeyName : string read GetKeyName write SetKeyName;
end;

Как было отмечено выше, мы закладываем возможность использования для доступа к INI-файлу два способа - через соответствующие свойства IniFileObj : TIniFile и IniFileName : string. Для использования IniFileName, мы должны предусмотреть создание и уничтожение объекта TIniFile самостоятельно. Так будет выглядеть реализация SetIniFile(const Value: TIniFile), при использовании IniFileObj:

procedure TIniCheckBox.SetIniFile(const Value: TIniFile);
begin
   FIniFileObj := Value;
   FIniFileName := FIniFileObj.FileName;
   FIniObjAssigned := True;
end;

Реализация методов SaveValToIni() и LoadValFromIni() выполнена следующим образом:

procedure TIniCheckBox.LoadValFromIni;
var
   bval : Boolean;
begin
   if not FIniObjAssigned then
     FIniFileObj := TIniFile.Create(FIniFileName);
   bval := FIniFileObj.ReadBool(FSectionName,FKeyName,False);
   Checked := bval;
   if not FIniObjAssigned then
     FIniFileObj.Free;
end;


procedure TIniCheckBox.SaveValToIni;
begin
   if not FIniObjAssigned then
     FIniFileObj := TIniFile.Create(FIniFileName);
   FIniFileObj.WriteBool(FSectionName, FKeyName, Checked);
   if not FIniObjAssigned then
     FIniFileObj.Free;
end;

TiniComboBox

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

TIniComboBox = class(TComboBox, IIniOperations)

Особенностью реализации компонента на основе TComboBox является только логика получения и сохранения значений списка элементов выбора. Для поддержки этой специфики необходимо соответствующим образом реализовать методы SaveValToIni() и LoadValFromIni():

procedure TIniComboBox.LoadValFromIni;
var
   sval : string;
   i : integer;
   tmplst : TStringList;
begin
   if not FIniObjAssigned then
     FIniFileObj := TIniFile.Create(FIniFileName);
   sval := FIniFileObj.ReadString(FSectionName,FKeyName,'');
   //считываем из ini значения ...
   tmplst := TStringList.Create;
   StrWordsS(sval,DelimSgn,tmplst);

   Text := tmplst.Strings[0];
   Items.Text := tmplst.Text;

   if not FIniObjAssigned then
     FIniFileObj.Free;
   tmplst.Free;
end;


procedure TIniComboBox.SaveValToIni;
var
   FullText : string;
   i : integer;
begin
   if not FIniObjAssigned then
   FIniFileObj := TIniFile.Create(FIniFileName);
   //сохраняем в строку все значения, первым будет из св-ва Text ...
   FullText := Text;
   for i := 0 to Items.Count - 1 do
     begin
       if Items[i] <> Text then
         begin
           FullText := FullText + DelimSgn + Items[i]
         end;
     end; //for ...
   // writing ...
   FIniFileObj.WriteString(FSectionName, FKeyName, FullText);
   if not FIniObjAssigned then
     FIniFileObj.Free;
end;

Форма настроек приложения

После выполнения реализации и установки наших компонент в палитру компонент Delphi можно создать приложение с формой Настроек и опробовать вновь созданные компоненты (см. пример к данной статье). Отметим лишь, как мы собираемся управлять чтением/записью значениями различных Настроек в форме Настроек используя унифицированное обращение к различным компонентам. Напишем следующий код, который позволит загрузить во все компоненты значения из INI-файла (предполагая, что переменная iniFile уже успешно создана как TIniFile):

procedure Tfm_Settings.FormShow(Sender: TObject);
var
   i : integer;
   ProxyI : IIniOperations;
begin
   Notebook1.ActivePage := 'Main';
   for i := 0 to ComponentCount -1 do
     begin
       if Components[i].GetInterface(IIniOperations, ProxyI) then
         begin
           ProxyI.IniFileObj := iniFile;
           ProxyI.LoadValFromIni();
         end;
     end;
end;

аналогично, при закрытии Формы Настроек мы сохраняем все изменения в INI-файле:

procedure Tfm_Settings.FormClose(Sender: TObject; var Action: TCloseAction);
var
  i : integer;
  ProxyI : IIniOperations;
begin
  if ModalResult = mrOK then
   begin
    for i := 0 to ComponentCount -1 do
     begin
      if Components[i].GetInterface(IIniOperations, ProxyI) then
       begin
        ProxyI.IniFileObj := iniFile;
        ProxyI.SaveValToIni();
       end;
     end;
   end;
end;

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

Вместо заключения

Данный способ (применение интерфейса) может быть использован для решения аналогичных задач, требующих унифицированного обращения к разнородным классам, без необходимости наличия общего предка. К статье прилагается package в котором реализованы некоторые компоненты основанные на данном способе получения значений Настроек. Это TiniLabeledEdit, TiniComboBox и TiniCheckBox. А так же приведен пример использования этих компонент в форме Настроек приложения (собрано в Delphi 7).

Одним из интересных расширений темы является абстрагирование от способа хранения параметров приложения (INI/Registry/Таблицы БД/ …) и возможность получения компонентами значений от абстрактных поставщиков информации. При решение такой задачи можно использовать паттерны проектирования, реализация которых на языке Delphi интересует многих разработчиков. Кроме этого возможно расширение списка компонент "информированных" о параметрах приложения за счет использования, например TradioButton, включения дополнительной логики мэппинга между "дружественными для пользователя" (user-friendly) названиями значений параметров и их названием в хранилище (a-la DisplayValue).

Copyright© 2003 Булуй Юрий  Специально для Delphi Plus

Rambler's Top100