Сохранение настроек

в 11:31, , рубрики: Delphi, Песочница, Программирование, метки: ,

0. Предисловие

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

1. Теория

С чего начинаются все проблемы с сохранением настроек? Верно, с визуальных компонентов. Ведь по сути, все значения динамических настроек хранятся в тех или иных полях визуальных компонентов. Осознав это, я задался вопросом: «А что нам это дает?» А вот что:

  1. У любого компонента есть имя
  2. У любого компонента есть поле для хранения значения
  3. Все компоненты на форме можно легко найти

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

2. Выбор хранилища

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

[название_блока1]
название_переменной1=значение_переменной1
название_переменной2=значение_переменной2

название_переменнойN=значение_переменнойN

[название_блока2]
название_переменной1=значение_переменной1
название_переменной2=значение_переменной2

название_переменнойN=значение_переменнойN

[название_блокаN]
название_переменной1=значение_переменной1
название_переменной2=значение_переменной2

название_переменнойN=значение_переменнойN

Эта структура идеально подходит для хранения наших настроек:

[тип_компонента]
название_компонента=значение

3. Реализация

Пожалуй, хватит пустых слов, перейдем к делу! Для написания примеров я буду пользоваться языком Pascal'евской группы. При желании, примеры можно будет легко портировать на любой другой язык. Итак, для начала рассмотрим наш основной инструмент — класс TIniFile:

Класс располагается в модуле IniFiles и позволяет легко работать с *.ini файлами. В классе предусмотрено создание *.ini файла, запись и чтение различных типов данных, значения по умолчанию, одним словом, все то, что нам нужно. Набросаем скелет процедуры сохранения настроек:

uses
	...IniFiles;  //Подключаем модуль с классом TIniFile
	
...

procedure SaveSettings;
begin
	//Создаем файл Settins.ini и класс для работы с ним
	with TIniFile.Create(ExtractFilePath(ParamStr(0)) + '/Settings.ini') do 
	begin
		try
			//Тут будем сохранять настройки
		finally
			Free;  //Освобождаем память выделенную под класс TIniFile  
		end;
	end;
end;

Ну вот, основа положена. Код простой, то на всякий случай разъясню ключевые моменты. Мы добавляем в блок uses юнит IniFiles, потом объявляем процедуру SaveSettings, создаем в ней экземпляр класса TIniFile, передаем в его конструктор имя файла, в который собираемся записывать настройки, а класс в свою очередь открывает этот файл, или же, если файла нет — создает его.

Примечания:

— О классе TIniFile.
— Функция ParamStr(0) возвращает полное имя нашего приложения.
— Функция ExtractFilePath достает из переданного ей параметра путь, без имени файла и закрывающего слеша.
— Класс TIniFile не присваивается никакой переменной, и будет доступен только внутри блока with, поэтому, очень важно добавить конструкцию try/finally, где память, выделенная под него, будет освобождена.

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

uses
	...IniFiles;
	
...

procedure SaveSettings(AOwner: TForm); //Передаем функции форму, на которой будем искать компоненты
var
	i: Integer; //Добавляем итератор для цикла
begin
	with TIniFile.Create(ExtractFilePath(ParamStr(0)) + '/Settings.ini') do 
	begin
		try
			for i := 0 to AOwner.ComponentCount - 1 do  //Пробежимся по всем компонентам формы
			begin
				if AOwner.Components[i] is TEdit then  //Если компонент типа TEdit, то
				begin
					WriteString  //Запишем в файл
					(
						'TEdit',  //В блок TEdit
						(AOwner.Components[i] as TEdit).Name,  //В переменну с именем компонента
						(AOwner.Components[i] as TEdit).Text  //Значение текстового поля
					);
				end;
			end;
		finally
			Free;   
		end;
	end;
end;

В этот раз, мы добавили нашей процедуре передаваемый аргумент AOwner — это форма на которой мы собираемся искать компоненты. Затем мы проходимся по всем компонентам формы и ищем среди них TEdit'ы, значения которых потом записываем в файл. В результате мы получим нечно подобное:

[TEdit]
Edit1=Hello
Edit2=World

Примечания:

— Свойство класса TForm.ComponentCount предоставляет нам информацию о количестве компонентов принадлежащих форме.
— О волшебных операторах is и as.

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

uses
	...IniFiles;
	
...

procedure SaveSettings(AOwner: TForm);
var
	i: Integer;
begin
	with TIniFile.Create(ExtractFilePath(ParamStr(0)) + '/Settings.ini') do 
	begin
		try
			for i := 0 to AOwner.ComponentCount - 1 do
			begin
				if AOwner.Components[i] is TEdit then
				begin
					WriteString
					(
						'TEdit',
						(AOwner.Components[i] as TEdit).Name,
						(AOwner.Components[i] as TEdit).Text
					);
				end;
				
				if AOwner.Components[i] is TCheckBox then
				begin
					WriteBool
					(
						'TCheckBox',
						(AOwner.Components[i] as TCheckBox).Name,
						(AOwner.Components[i] as TCheckBox).Checked
					);
				end;
				
				if Owner.Components[i] is TDateTimePicker then
				begin
					WriteInteger
					(
						'TDateTimePicker', 
						(AOwner.Components[i] as TDateTimePicker).Name,
						Round(Int((AOwner.Components[i] as TDateTimePicker).Date))
					);
				end;
			end;
		finally
			Free;   
		end;
	end;
end;

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

uses
	...IniFiles;
	
...

procedure SaveSettings(AOwner: TForm);

...

procedure LoadSettings(AOwner: TForm);
var
	i: Integer;
begin
	with TIniFile.Create(ExtractFilePath(ParamStr(0)) + '/Settings.ini') do 
	begin
		try
			for i := 0 to AOwner.ComponentCount - 1 do
			begin
				if AOwner.Components[i] is TEdit then
				begin
					(AOwner.Components[i] as TEdit).Text := ReadString  //Читаем в текствое поле компонента
					(
						'TEdit',  //Из блока TEdit
						(AOwner.Components[i] as TEdit).Name, //Значение переменной с именем компонента
						(AOwner.Components[i] as TEdit).Text //Если значения в файле нет, оставим значение по умолчанию
					);
				end;
			end;
		finally
			Free;   
		end;
	end;
end;

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

Вот собственно и все, основа готова. Но можно ли на этом закончить?

4. Подводные камни

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

  • Обработчики событий изменения значений
  • Сохранение переносов строки

Итак, рассмотрим проблемы по порядку.

Обработчики событий

Часто так бывает, что после ввода данных пользователем, программа сразу же делает какие то действия: подгружает список вариантов для какого нибудь TComboBox, делает активными или неактивными какие нибудь элементы формы, что то переключает, в общем, проявляет интерактивность. И естественно, что пользователь после загрузки своих настроек ожидает увидеть программу в полной боевой готовности, то есть, списки вариантов уже должны быть подгружены, нужные кнопки активными, не нужные неактивными. Добиться этого можно вызывая повешенные на компоненты обработчики после загрузки настроек. Конечно же, не все так просто и придется учесть несколько моментов. Немного изменим нашу процедуру LoadSettings:

uses
	...IniFiles;
	
...

procedure SaveSettings(AOwner: TForm);

...

procedure LoadSettings(AOwner: TForm);
var
	i: Integer;
begin
	with TIniFile.Create(ExtractFilePath(ParamStr(0)) + '/Settings.ini') do 
	begin
		try
			for i := 0 to AOwner.ComponentCount - 1 do
			begin
				if AOwner.Components[i] is TCheckBox then
				begin
					(AOwner.Components[i] as TCheckBox).Checked := ReadBool
					(
						'TCheckBox', 
						(AOwner.Components[i] as TCheckBox).Name, 
						(AOwner.Components[i] as TCheckBox).Checked
					);

					if Assigned((AOwner.Components[i] as TCheckBox).OnClick) then  //Проверяем, привязан ли обработчик
					begin
						(AOwner.Components[i] as TCheckBox).OnClick(AOwner.Components[i]);  //Исполняем его
					end;
				end;
			end;
		finally
			Free;   
		end;
	end;
end;

Итак, для примера, возьмем компонент TCheckBox и обработчик его события OnClick. После того, как мы прочитали значение компонента, проверяем, повешен ли обработчик на его событие OnClick, если таковой имеется, мы его исполняем, передав ему в качестве аргумента Sender сам компонент.

Аналогичным образом можно вызвать разные обработчики событий для разных компонентов. На этом первый подводный камень пройден.

Примечания:

— О функции Assingned.
— Следует отметить, что проверить наличие обработчика очень важно, потому как попытка исполнения несуществующего обработчика приведет к ошибке.

Сохранение переносов строки

Пример этой проблемы — необходимость сохранить многострочный текст из TMemo, Тут то нас и подстерегает конфуз. Текст, конечно, сохранится, но при попытке прочитать его обратно мы получим только лишь первую строку. Это связано с тем, что в структуре *.ini файлов значение читается до переноса каретки. Один из способов преодоления этой преграды, это заменить все символы переноса строки и перевода каретки на какой нибудь служебный символ, который совершенно точно не встретится в обычном тексте. Я для этих целей использую первый ASCII код. Подправим процедуры чтения и записи:

uses
	...IniFiles;
	
...

procedure SaveSettings(AOwner: TForm);
var
	i: Integer;
begin
	with TIniFile.Create(ExtractFilePath(ParamStr(0)) + '/Settings.ini') do 
	begin
		try
			for i := 0 to AOwner.ComponentCount - 1 do
			begin
				if AOwner.Components[i] is TCheckBox then
				begin
					if Owner.Components[i] is TMemo then
					begin
						//Заменяем перенос каретки и строки на первый ASCII код
						WriteString
						(
							'TMemo', 
							(AOwner.Components[i] as TMemo).Name, 
							ReplaceSubString((AOwner.Components[i] as TMemo).Text, #$01, #$0D + #$0A)
						);
					end;
				end;
			end;
		finally
			Free;   
		end;
	end;
end;

procedure LoadSettings(AOwner: TForm);
var
	i: Integer;
begin
	with TIniFile.Create(ExtractFilePath(ParamStr(0)) + '/Settings.ini') do 
	begin
		try
			for i := 0 to AOwner.ComponentCount - 1 do
			begin
				if AOwner.Components[i] is TCheckBox then
				begin
					if AOwner.Components[i] is TMemo then
					begin
						//Заменяем первый ASCII код на перенос каретки и строки
						(AOwner.Components[i] as TMemo).Text := ReplaceSubString
						(
							ReadString
							(
								'TMemo', 
								(AOwner.Components[i] as TMemo).Name, 
								(AOwner.Components[i] as TMemo).Text
							),
							#$01, 
							#$0D + #$0A
						);
					end;
				end;
			end;
		finally
			Free;   
		end;
	end;
end;

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

Примечания:

— Таблица ASCII кодов.
— Функция ReplaceSubString:

function ReplaceSubString(ASource, AOld, ANew: String): String;
var
	Position: Integer;
	Temporary: String;
begin
	Position := Pos(AOld, ASource);
	Temporary := '';

	while (Position <> 0) do
	begin
		Temporary := Temporary + Copy(ASource, 1, Position - 1) + ANew;
		Delete(ASource, 1, Position + Length(AOld) - 1);
		Position := Pos(AOld, ASource);
	end;

	Result := Temporary + ASource;
end;

5. Послесловие

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

Автор: Cyapa

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js