Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

Visual C++ - расширенное программирование Библиотека для работы с параметрами конфигурации приложений


Информационный Канал Subscribe.Ru

Visual C++ - расширенное программирование

Выпуск № 11
Cайт : SoftMaker.com.ru
Архив рассылки : SoftMaker.com.ru
Количество подписчиков : 3564
В этом выпуске
От автора

Здравствуйте, уважаемые подписчики !

Рад снова приветствовать вас на страницах этой рассылки. Как вы могли заметить, она не выходила очень давно (различные обстоятельства тому виной). Но теперь работа рассылки снова возобновилась.

Как и прежде, здесь вас ждут интересные статьи, посвященные различным аспектам программирования под Windows на C/C++ (часто - с использованием MFC, но и не только), готовые исходники полезных классов, компонентов, функций.

Сайт рассылки: http://SoftMaker.com.ru - там вы сможете найти новые статьи и архив рассылки, описываемые здесь исходники.

Работает форум: http://forum.SoftMaker.com.ru

Анонс: дискуссионные листы компьютерной тематики

Многие наверно знают о существовании в рамках службы Subscribe.ru сервиса дискуссионных листов (или - почтовых конференций). Дискуссионные листы - это рассылки, содержимое которых формируется самими подписчиками. Принцип прост - Вы пишете письмо по некоторому адресу, и почти сразу всем подписчикам будет разослан очередной выпуск дискуссионного листа, содержащего Ваше сообщение. Участники отвечают на ваше письмо так же - отправляя письма на адрес конференции. Таким способом можно вести обсуждение различных тем сразу со всеми участниками дискуссионной группы. Благодаря высокой оперативности общения, а также доступности и удобству (для общения Вам достаточно только электронной почты) - сервис дискуссионных листов Subscribe.ru быстрыми темпами приобретает все большую популярность среди подписчиков.

В дискуссионных листах (почтовых конференциях) очень просто принять участие, подписавшись на них. Подписаться можно либо по почте (послав даже пустое письмо на адрес подписки), либо на сайте subscribe.ru, либо посредством форм, приведенных ниже.

Самый популярный дискуссионный лист по программированию на subscribe.ru:
  Программирование. Форум !!!    
Подписаться по почте
существует практически с момента создания сервиса листов (уже больше года), начитывает ~470 подписчиков.


Недавно открывшийся дискуссионный лист "Скрипты для сайта":
  Скрипты для сайта    
Подписаться по почте
посвящен поиску, разработке, обсуждению скриптов, обмену скриптами (как клиентскими - на JS, VBScript, так и серверными - на PHP, Perl, ASP, и.т.д.). Если не знаете, где взять нужный скрипт - обращайтесь прямо сюда.

В конце данного номера также приведен список листов компьютерной тематики, которые могут быть интересны для вас.

Как всегда, Вы можете отправить свои пожелания по поводу рассылки и сайта по этому адресу.

Если вы хотите опубликовать свою статью, исходники, либо создать и вести какой либо раздел в этой рассылке - также пишите мне.

С уважением, Вахтуров Виктор.

MFC - простое и сложное

Библиотека классов для работы с параметрами конфигурации приложений
Вместо вступления

Сегодня мы рассмотрим такую небольшую (но весьма актуальную) тему как сохранение и загрузка параметров конфигурации приложения.

Как известно, традиционно существует два способа хранить параметры конфигурации приложений Windows: в системном реестре и конфигурационных (ini) файлах. Для обоих случаев существует специальное API, причем API для работы с файлами конфигурации унаследовано операционными системами семейств Windows 95 и Windows NT от ранних версий Windows и считается устаревшим.

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

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

---

Итак, сегодня мы разработаем небольшую библиотеку классов для работы с "настройками" (параметрами конфигурации) приложения, создав простой, удобный и расширяемый программный компонент.

Постановка задачи

Для начала давайте сразу оговорим функциональность разрабатываемой библиотеки. Изначально будем ориентироваться на сохранение параметров конфигурации как в реестре, так и в ini-файлах.

Программный компонент должен "уметь":

  • Сохранять: числовые, текстовые, бинарные (произвольной длины) и бинарные (фиксированной длины - структуры) данные.
  • Интерфейс сохранения/загрузки данных должен быть одинаков как для реестра, так и для файлов конфигурации.
  • Библиотека должна предусматривать обработку ошибок и коррекцию данных, как перед сохранением, так и после загрузки (на случай, например, если были считаны искаженные данные).
  • В библиотеке должна присутствовать возможность получения информации о причине ошибки сохранения или загрузки данных.

Все вышесказанное укладывается в достаточно простую иерархию классов.

Корнем иерархии будет являться некий класс (назовем его, например, CVSettings), реализующий методы сохранения/загрузки параметров (например, Save и Load соответственно) и виртуальные функции-члены, которые должны быть перегружены в дочерних классах (например, CVRegSettings и CVIniFileSettings) для реализации работы по загрузке/сохранению данных для конкретного случая (реестр или файл).

Средства для работы с параметрами конфигурации приложения

Для начала давайте ознакомимся со средствами, которые можно было бы использовать для решения наших задач.
Первое, про что надо сказать - это API Windows для работы с системным реестром и файлами параметров конфигурации. Как дальше будет показано, возможности, предоставляемые этим API далеки от идеала.
Второе средство, которое я сегодня рассмотрю - это методы класса CWinApp для записи/чтения параметров конфигурации. Они (естественно) также основаны на использовании вышеназванного API, но вводят некоторые дополнительные ограничения на объем записываемых данных.

API для работы с параметрами конфигурации

Для работы с реестром и файлами параметров конфигурации приложений существует группа API функций, всесторонне документированная и описанная в MSDN.

Реестр

Для записи в реестр и чтения из реестра можно использовать Win-API функции:

RegQueryValueEx и
RegSetValueEx соответственно.

Данные функции полностью удовлетворяют нашим потребностям, так как обеспечивают чтение/запись числовых, текстовых и бинарных данных. Правда, при их использовании следует учесть ограничения, накладываемые на объемы данных, которые могут быть сохранены как параметры в реестре.

Информация из MSDN:

Пределы размеров элементов реестра:

Длинные значения (больше чем 2048 байт) должны быть сохранены как файлы с именами этих файлов, сохраненных в реестре. Это поможет работать с реестром эффективно.

Максимальный объем параметра может быть следующим:
Windows NT/2000/XP: Доступная память
Windows 95/98/Me: 16,300 байт. Существует ограничение в 64 килобайта на общий объем данных ключа реестра.

К тому же следует учесть ограничения на длины имен параметров и ключей реестра:

длина имени ключа: 255 символов

длины имен параметров:
Windows XP, Windows .NET Server 2003 family: 16383 символа
Windows 2000: 260 ANSI - символов, или 16383 Unicode - символов
Windows 95/98/Me: 255 символов

То есть следует учесть, что в Windows 95/98/Me существуют весьма жесткие ограничения на объем данных, сохраняемых в реестре. Также существуют ограничения на длину имен параметров.

Файлы параметров конфигурации (ini-файлы)

Работа с ini-файлами немного более проблематична. Рассмотрим основные трудности, с которыми нам придется столкнуться.

  • Для чтения числового параметра можно использовать функцию:
    GetPrivateProfileInt, а для записи числового параметра функции нет - придется форматировать число в строку, а уже строку записывать в конфигурационный файл.
  • Для записи/чтения структур (бинарных данных фиксированного размера) существуют функции:
    WritePrivateProfileStruct и
    GetPrivateProfileStruct,
    а вот для работы с бинарными данными произвольного размера (хотя бы чтения, так как записать можно и при помощи WritePrivateProfileStruct) - функций, к сожалению, нет.
  • Для строк существуют функции: WritePrivateProfileString и
    GetPrivateProfileString.
    Но они ограничивают объем читаемой/записываемой информации 32-мя килобайтами, а также в Windows 95 не поддерживаются символы табуляции в записываемой строке.

Это практически всё API, которое мы можем использовать для решения поставленной задачи.

Методы класса CWinApp

Опишу их лишь вкратце (так как использовать мы их не будем). Упомянуть их здесь заставляет только чувство справедливости.
Для работы с параметрами конфигурации класс CWinApp предоставляет методы:



BOOL WriteProfileInt(LPCTSTR lpszSection, LPCTSTR lpszEntry, int nValue); UINT GetProfileInt(LPCTSTR lpszSection, LPCTSTR lpszEntry, int nDefault); BOOL WriteProfileString(LPCTSTR lpszSection, LPCTSTR lpszEntry, LPCTSTR lpszValue); CString GetProfileString(LPCTSTR lpszSection, LPCTSTR lpszEntry, LPCTSTR lpszDefault = NULL); BOOL WriteProfileBinary(LPCTSTR lpszSection, LPCTSTR lpszEntry, LPBYTE pData, UINT nBytes); BOOL GetProfileBinary(LPCTSTR lpszSection, LPCTSTR lpszEntry, BYTE** ppData, UINT* pBytes);

Все эти методы используют вышеописанное API для доступа к реестру и ini-файлам. Есть один большой недостаток, который может мешать использовать эти функции по своему усмотрению. Дело в том, что данные методы спроектированы преимущественно для работы с чем то одним - либо с ini-файлами, либо с реестром. Класс CWinApp содержит переменные-члены m_pszRegistryKey - для хранения имени подключа реестра, и m_pszProfileName - для хранения пути к конфигурационному файлу. Каждая из вышеописанных функций проверяет, отлично ли от NULL значение m_pszRegistryKey и, если это так, то работает с реестром. Иначе - с файлом конфигурации. Согласитесь, не так приятно управлять поведением этих функций, присваивая значения переменным-членам.

Еще один большой недостаток - очень ограниченные объемы данных, которые можно сохранить/загрузить с помощью функций WriteProfileString GetProfileString, WriteProfileBinary и GetProfileBinary.

Если взглянуть на исходный код этих функций, то можно, например, заметить, что, в методе GetProfileString при чтении их файла конфигурации под данные выделяется буфер фиксированной длины 4096 символов (ASCII или UNICODE). А метод GetProfileBinary использует GetProfileString для чтения текстового дампа бинарных значений из ini-файла. Это означает, что бинарных данных при помощи этой функции можно считать не более чем 2^11 - 1 байт.

Создание библиотеки
Принцип работы

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

Я подходил со своих позиций, а именно: обычно для хранения параметров конфигурации я описываю структуру, содержащую поля для всех необходимых данных. В приложении создается один статический экземпляр данной структуры (либо как переменная-член объекта класса приложения (для проектов на базе MFC), либо - глобальная переменная, если проект на Win-API). Обычно также я пишу две функции - что-то типа: LoadSettings и SaveSettings - для загрузки и, соответственно, сохранения параметров. Думаю, так делают очень многие.

Логично было бы инкапсулировать данные и методы загрузки/сохранения в одном классе. Также хотелось бы унифицировать сам процесс чтения/записи данных таким образом, чтобы не приходилось каждый раз реализовывать функции типа Load и Save.

Небольшое отступление. Вспомним, как реализован обмен данными между элементами управления диалоговых окон и переменными-членами классов диалогов в MFC. Там это называется DDX (Dialog Data Exchange). Все, конечно, знают о существовании виртуальной функции-члена класса CDialog::DoDataExchange. Для обмена данными надо просто реализовать данную функцию в своем классе диалога и вызывать оттуда функции вида DDX_*, "мапящие" (отображающие) данные контролов в переменные, или наоборот.

Этим же путем решил пойти и я.

То есть, использование библиотеки должно быть примерно таким.
Например, для хранения параметров конфигурации приложения я хочу использовать ini-файлы.
Библиотека предоставляет класс для работы ini-файлами CVIniFileSettings. Я просто наследую некоторый свой класс от CVIniFileSettings, включаю в него все необходимые приложению данные в виде переменных-членов этого класса. Реализую виртуальную функцию обмена данными (пусть она называется DoSettingsExchange), а в ней вызываю некоторые функции (по аналогии с функциями DDX_* диалогов) для записи/чтения данных. Далее я просто создаю объект этого класса и вызываю его методы Load и Save для чтения и записи настроек.

Теперь приступим к реализации.

Описание реализации
Базовый класс (CVSettings)

В качестве базового класса библиотеки создан класс CVSettings.
Вот его объявление:



class CVSettings { public: CVSettings(); virtual ~CVSettings(); public: virtual BOOL Load(); virtual BOOL Save(); DWORD GetErrorCode(); CString GetErrorString(); BOOL IsVariableChanged(void *pVariable); protected: void SetError(DWORD dwLastError); void SetError(LPCTSTR lpszError); void ClearError(); virtual void ValidateData(BOOL bSave); virtual void DoSettingsExchange(BOOL bSave); protected: // Функции отображения переменных на внешние данные void SX_MAP_Int(BOOL bSave, LPCTSTR lpszEntry, int &value); void SX_MAP_Long(BOOL bSave, LPCTSTR lpszEntry, long &value); void SX_MAP_Uint(BOOL bSave, LPCTSTR lpszEntry, UINT &value); void SX_MAP_Text(BOOL bSave, LPCTSTR lpszEntry, LPTSTR lpszText, int nMaxSize); void SX_MAP_Text(BOOL bSave, LPCTSTR lpszEntry, CString &string); void SX_MAP_DWord(BOOL bSave, LPCTSTR lpszEntry, DWORD &value); void SX_MAP_Struct(BOOL bSave, LPCTSTR lpszEntry, LPVOID pData, DWORD dwDataSize); void SX_MAP_Binary(BOOL bSave, LPCTSTR lpszEntry, LPBYTE &rpbData, DWORD &dwDataSize); // Функции физического чтения/записи virtual BOOL RW_Text(BOOL bSave, LPCTSTR lpszEntry, CString &string) = 0; virtual BOOL RW_DWord(BOOL bSave, LPCTSTR lpszEntry, DWORD &value) = 0; virtual BOOL RW_Struct(BOOL bSave, LPCTSTR lpszEntry, LPVOID pData, DWORD dwDataSize); virtual BOOL RW_Binary(BOOL bSave, LPCTSTR lpszEntry, LPBYTE &rpbData, DWORD &dwDataSize) = 0; protected: DWORD m_dwLastError; CString m_strLastError; static const LPCTSTR m_aErrorMessages[]; CMap <void*, void*, BOOL, BOOL> m_mapChangedVariables; };

Как видите, кроме виртуальных методов для загрузки и сохранения (Load и Save), объявлено еще много чего. Разберемся, для чего это нужно.

Виртуальные функции:

CVSettings::DoSettingsExchange и
CVSettings::ValidateData

должны быть реализованы в дочерних классах. Базовая реализация методов Load и Save вызывает эти функции при сохранении/загрузке.

Функция ValidateData вызывается до сохранения или непосредственно после загрузки параметров. Ее назначение состоит только в том, чтобы реализовать коррекцию считанных, или подготовленных для записи данных. Базовая реализация этой функции - пуста. В классе CVSettings функция ValidateData - просто заглушка. Мы должны будем перегружать ее в классах-наследниках, если нам понадобится проверка корректности данных.

Функция DoSettingsExchange (аналог DoDataExchange для класса CDialog). В ней, как было сказано выше, и происходит процесс чтения/записи параметров. DoSettingsExchange также вызывается базовой реализацией функций Load и Save. Передаваемый в функцию параметр bSave указывает на то, должны ли данные сохраняться, либо читаться. В CVSettings реализация DoSettingsExchange также пуста (мы будем реализовывать эту функцию в классах-наследниках).

В классах MFC, производных от CDialog обмен данными между переменными-членами и элементами управления, происходит посредством вызова функций вида DDX_*. Например, чтобы реализовать "трансфер" данных от элемента управления в переменную типа CString, из функции DoDataExchange производится вызов функции DDX_Text.

В классе CVSettings реализован набор аналогичных функций для оперирования основными типами данных:



void SX_MAP_Int(BOOL bSave, LPCTSTR lpszEntry, int &value); void SX_MAP_Long(BOOL bSave, LPCTSTR lpszEntry, long &value); void SX_MAP_Uint(BOOL bSave, LPCTSTR lpszEntry, UINT &value); void SX_MAP_Text(BOOL bSave, LPCTSTR lpszEntry, LPTSTR lpszText, int nMaxSize); void SX_MAP_Text(BOOL bSave, LPCTSTR lpszEntry, CString &string); void SX_MAP_DWord(BOOL bSave, LPCTSTR lpszEntry, DWORD &value); void SX_MAP_Struct(BOOL bSave, LPCTSTR lpszEntry, LPVOID pData, DWORD dwDataSize); void SX_MAP_Binary(BOOL bSave, LPCTSTR lpszEntry, LPBYTE &rpbData, DWORD &dwDataSize);

также определены макросы:



#define SX_MAP_Bool SX_MAP_Int #define SX_MAP_Structure(bSave, lpszEntry, structure) \ SX_MAP_Struct(bSave, lpszEntry, &structure, sizeof(structure)) #define SX_MAP_Array(bSave, lpszEntry, array) \ SX_MAP_Struct(bSave, lpszEntry, &array[0], sizeof(array))

Нетрудно заметить, что SX_MAP_Int, SX_MAP_Long, SX_MAP_Uint, SX_MAP_DWord оперируют числами, SX_MAP_Text - 2 реализации - для работы со строками с завершающим нулем и с CString. То есть, алгоритмы работы с этими типами данных можно реализовать на меньшем подмножестве функций. Что и сделано.

Для "физического" чтения/записи данных в классе CVSettings определены виртуальные функции:



virtual BOOL RW_Text(BOOL bSave, LPCTSTR lpszEntry, CString &string) = 0; virtual BOOL RW_DWord(BOOL bSave, LPCTSTR lpszEntry, DWORD &value) = 0; virtual BOOL RW_Struct(BOOL bSave, LPCTSTR lpszEntry, LPVOID pData, DWORD dwDataSize); virtual BOOL RW_Binary(BOOL bSave, LPCTSTR lpszEntry, LPBYTE &rpbData, DWORD &dwDataSize) = 0;

Они должны реализовывать конкретные алгоритмы для чтения/записи данных в классах-наследниках.
Как видите, три из четырех - это чисто виртуальные функции (pure virtual functions). Таким образом, класс CVSettings - это абстрактный базовый класс (создать его объект нельзя).
Функции RW_Text, RW_DWord и RW_Binary обязательно должны будут быть реализованы в дочерних классах. Функция RW_Struct реализована в классе CVSettings посредством использования RW_Binary (что, вобщем то, логично - ведь структура - это просто бинарные данные заранее определенной длины).

В классе CVSettings также реализованы функции получения информации об ошибке загрузки/сохранения:


DWORD GetErrorCode(); CString GetErrorString();

функции для установки и сохранения кода ошибки, либо сообщения об ошибке:


void SetError(DWORD dwLastError); void SetError(LPCTSTR lpszError);

и функция для проверки того, был ли конкретный параметр сохранен/загружен во время последней операции сохранения или загрузки:


BOOL IsVariableChanged(void *pVariable);

Этот метод выбирает значение успешности/неудачи последнего вызова функций RW_* для конкретной переменной из хеш-массива (карты) m_mapChangedVariables (переменная-компонента класса CVSettings). Значения заносятся в этот массив при вызове методов SX_MAP_*, то есть в процессе сохранения или загрузки. В методах Load и Save базовой реализации (до вызова) DoSettingsExchange массив очищается.

Теперь давайте рассмотрим более интересные вещи - классы библиотеки:

CVRegSettings - для сохранения параметров в реестре и
CVIniFileSettings - для работы с файлами конфигурации приложений (ini-файлами).

Класс для работы с параметрами конфигурации, сохраняемыми в реестре (CVRegSettings)

В данном классе реализованы функции: RW_Text, RW_DWord, RW_Binary записи и чтения данных реестра.

Здесь, вобщем то все достаточно просто. Используется обычное API для работы с реестром. Что необходимо упомянуть, так это то, что класс CVRegSettings имеет две версии конструктора:


CVRegSettings(HKEY hKeyParent, LPCTSTR lpszSubKey); CVRegSettings(CWinApp *pApplication, LPCTSTR lpszSubKey);

Вы можете создавать объект класса CVRegSettings либо передавая уже открытый Вами дескриптор ключа реестра (первый вариант), либо указатель на объект класса приложения MFC (второй вариант). Необходимый ключ реестра открывается (либо создается) функцией CVRegSettings::CreateRegistryKey. Если был использован второй вариант конструктора, то базовый ключ реестра для хранения параметров приложения будет запрошен у объекта приложения (метод CWinApp::GetAppRegistryKey).

Класс для работы с параметрами конфигурации, сохраняемыми в INI-файлах (CVIniFileSettings)

Класс имеет один конструктор с двумя параметрами (так как оба параметра заданы по умолчанию, то этот же конструктор выступает и в роли default-конструктора). В качестве параметров в конструктор передаются: путь к файлу параметров конфигурации и имя секции в файле, в которую будут сохраняться данные. Если вместо параметра (любого) передано NULL-значение, то параметр запрашивается у объекта класса приложения.

В классе реализованы три функции физического чтения/записи данных: RW_Text, RW_DWord, RW_Binary (как и для класса CVRegSettings, реализация RW_Struct наследуется).

CVIniFileSettings::RW_Text
Это самая простая из вышеперечисленных функций, так как использует для чтения/записи строк в ini-файл API-функции WritePrivateProfileString и GetPrivateProfileString. В ней также происходит проверка на наличие в записываемой строке символов \n \r и \t (напомню, что символ табуляции не поддерживается в Win95 для строки, записываемой через WritePrivateProfileString, а \r и \n при чтении из ini-файла будут означать конец значения параметра).

CVIniFileSettings::RW_Dword
Как я уже отмечал, для записи числового параметра в ini-файл не существует специальной API-функции. Поэтому при записи числового значения RW_Dword конвертирует его в строку, а затем записывает с помощью WritePrivateProfileString.
С чтением числового параметра все еще более интересно. Для чтения параметра-числа из ini-файла существует API-функция GetPrivateProfileInt, однако, она не поможет нам. Дело в том, что используя эту функцию, нельзя быть уверенным в том, что значение указанного параметра действительно было считано. Так как, если параметр не был найден в файле конфигурации, то функция возвратит default-значение (передаваемое ей в качестве ее третьего параметра). По этой причине значения числовых параметров конфигурации читаются как строки (функцией GetPrivateProfileString), а затем конвертируются в число. Для конвертирования строки в числовое значение используется функция StringToDWord - пришлось писать "обертку" для функции strtoul (wcstoul), чтобы иметь возможность контролировать успешность операции конвертирования. Быть может, эта функция пригодится и Вам. Вот ее код:



BOOL StringToDWord(LPCTSTR lpszString, DWORD &refRet) { if(lpszString == NULL) return FALSE; if(lpszString[0] == _T('\0')) return FALSE; errno = 0; DWORD dwRet; LPTSTR lpszStrEnd = NULL; #ifdef _UNICODE dwRet = wcstoul(lpszString, &lpszStrEnd); #else dwRet = strtoul(lpszString, &lpszStrEnd, 0); #endif if(errno == ERANGE) return FALSE; if((lpszString == lpszStrEnd) || (*lpszStrEnd != '\0')) return FALSE; refRet = dwRet; return TRUE; }

CVIniFileSettings::RW_Binary
Как уже было сказано, для записи бинарных данных в ini-файл можно использовать API-функцию WritePrivateProfileStruct. Для чтения - GetPrivateProfileStruct. Однако, не все так просто.

Я поэкспериментировал с использованием этих функций, и вот что выяснилось:

  • WritePrivateProfileStruct может записать не так уж и много данных. У меня (под управлением ОС Windows 2000) получилось сохранить чуть меньше чем 514 килобайт (странно, почему такое "неровное" число; почему не 512, а именно - 514... да, пути Microsoft неисповедимы).
  • GetPrivateProfileStruct может считать гораздо меньше, чем можно записать при помощи WritePrivateProfileStruct. Буквально это - 2^15 - 2 байт (или - 32 килобайта - 2 байта).
  • При попытке перезаписи значения ключа секции ini-файла бинарными данными объемом больше чем 2^15 - 2 байт (при помощи все той же функции WritePrivateProfileStruct), происходит не перезапись, а "дописывание" новых данных в конец предыдущего блока. Вследствие этого, следующая попытка чтения завершается неудачей.
Таким образом, существуют проблемы с чтением данных и перезаписью значений данными, объемом больше 32-х килобайт.

В функции CVIniFileSettings::RW_Binary эти проблемы решаются.

Приведу код самой функции:



BOOL CVIniFileSettings::RW_Binary(BOOL bSave, LPCTSTR lpszEntry, LPBYTE &rpbData, DWORD &dwDataSize) { BOOL bSuccess = FALSE; if(bSave) { if(((rpbData != NULL) && (dwDataSize <= (1 << 29))) || (rpbData == NULL)) { if(WritePrivateProfileStruct((LPCTSTR) m_strSectionName, lpszEntry, NULL, (UINT) 0, (LPCTSTR) m_strIniFilePath)) { if(rpbData != NULL) { bSuccess = WritePrivateProfileStruct( (LPCTSTR) m_strSectionName, lpszEntry, rpbData, (UINT) dwDataSize, (LPCTSTR) m_strIniFilePath); } else bSuccess = TRUE; } } } else { LPBYTE pData = NULL; bSuccess = GetPrivateProfileBinary(lpszEntry, pData, dwDataSize); if(bSuccess) { if(rpbData) delete [] rpbData; rpbData=pData; } } return bSuccess; }

Проблема, описанная выше в П3. Решается путем вызова WritePrivateProfileStruct дважды (см. код) - сначала вместо указателя на буфер данных передается NULL, а затем (второй вызов) - указатель на записываемые данные. Вместо попытки перезаписи данных, мы сначала удаляем старые данные (согласно документации, в случае передачи NULL в качестве параметра на записываемые данные, WritePrivateProfileStruct просто удалит указанный ключ указанной секции ini-файла), а потом - пишем новые.

Проблема невозможности чтения больших объемов данных (описанная в П2) решается сложнее. Пришлось писать свою функцию (CVIniFileSettings::GetPrivateProfileBinary) для чтения бинарных данных из ini-файла. Не буду приводить тут ее код. Он хоть не очень большой, но и не очень маленький. Скажу только, что ini-файл отображается в память (см. API-функции CreateFileMapping, MapViewOfFile), затем производится поиск нужной секции и нужного ключа, и, собственно, чтение данных.

Тут, пожалуй, следует отметить, что бинарные данные записываются в ini-файл текстовым дампом. То есть, числовое значение каждого байта конвертируется в строку из двух символов (шестнадцатиричное представление) и подряд записывается в файл. Последние 2 символа такой последовательности представляют собой контрольную сумму записанного блока данных.

Для более четкого представления о том, как работает CVIniFileSettings::GetPrivateProfileBinary - смотрите исходный ее код.

Последнее, о чем стоит упомянуть - метод CVIniFileSettings::Load вызывает функцию CVIniFileSettings:: OpenIniFileBeforeRead, которая открывает целевой ini-файл для чтения (с тем, чтобы заблокировать файл для записи другими процессами). Файл закрывается после чтения данных.

Описание применения

Теперь давайте рассмотрим, как выглядит применение этой небольшой библиотечки на практике. Для демонстрации ее возможностей я создал небольшое демонстрационное приложение.

Демонстрационное приложение SettingsDemo

Проект создавался обычным "визардом" создания проектов MFC AppWizard (EXE) как однодокументное приложение без поддержки архитектуры Document/View. Далее я удалил из проекта сгенерированный мастером класс CChildView (понаследован от CWnd), заменив его своим классом диалогового окна CFormDialog. Получилось приложение с главным окном-рамкой с диалогом внутри (эффект как от применения архитектуры Document/View с классом View, отнаследованным от CFormView, только - нет ничего лишнего (типа класса документа)).

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

Я также создал небольшой класс - CPaintWnd (наследован от CStatic). Он немного дополняет стандартный элемент управления Static, позволяя рисовать на нем, просто удерживая кнопку мыши. Я также разместил один такой элемент управления в созданном диалоге. Бинарные данные картинки (в виде аппаратно-независимого растра) также будут сохраняться в файле параметров конфигурации. Таким образом будет продемонстрировано использование функциональности класса CVIniFileSettings.

Использование же функциональности класса CVRegSettings будет продемонстрировано на примере сохранения координат и состояния главного окна-рамки приложения.

Итак, все по порядку.

Пример использования класса CVRegSettings

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



class CAppSettings : public CVRegSettings { public: CAppSettings(); virtual ~CAppSettings(); protected: virtual void ValidateData(BOOL bSave); virtual void DoSettingsExchange(BOOL bSave); public: // Позиция главного окна WINDOWPLACEMENT m_sMainWindowPosition; };

Позиция и состояние главного окна приложения будет сохраняться в переменной m_sMainWindowPosition - структуре типа WINDOWPLACEMENT. Это очень удобно, поскольку для "запоминания" информации о положении окна достаточно вызвать всего одну функцию: GetWindowPlacement. Также для восстановления позиции окна достаточно вызвать SetWindowPlacement, передав ей указатель на заполненную структуру.

Как видно из объявления класса, в нем будут реализованы 2 функции: ValidateData и DoSettingsExchange. Просто приведу их реализацию, так как она просто банальна:



void CAppSettings::ValidateData(BOOL bSave) { m_sMainWindowPosition.length = sizeof(WINDOWPLACEMENT); if( (m_sMainWindowPosition.showCmd < SW_SHOWNORMAL) || (m_sMainWindowPosition.showCmd > SW_MAX)) { ZeroMemory(&m_sMainWindowPosition, sizeof(WINDOWPLACEMENT)); m_sMainWindowPosition.length = sizeof(WINDOWPLACEMENT); m_sMainWindowPosition.showCmd = SW_SHOWNORMAL; } } void CAppSettings::DoSettingsExchange(BOOL bSave) { SX_MAP_Structure(bSave, "MainWndPos", m_sMainWindowPosition); }

Как видите, очень просто - вся работа совершается в методах базового класса.

Далее - просто добавим объект этого класса (назовем его, например, m_sAppSettings) в объявление класса главного окна-рамки CMainFrame.

При закрытии окна (в обработчике сообщения WM_CLOSE) будем сохранять параметры:



void CMainFrame::OnClose() { if(GetWindowPlacement(&m_sAppSettings.m_sMainWindowPosition)) m_sAppSettings.Save(); CFrameWnd::OnClose(); }

Загрузку параметров и установку позиции окна будем производить в функции CMainFrame::CreateMainFrame, сразу после его создания.

Пример использования класса CVIniFileSettings

Создадим класс, наследованный от CVIniFileSettings (объект этого класса m_sFormSettings добавим в объявление класса диалогового окна CFormDialog):



class CFormSettings : public CVIniFileSettings { public: CFormSettings(); virtual ~CFormSettings(); public: UINT m_nNumber; CString m_strString; BYTE *m_pDibData; DWORD m_dwDibDataSize; protected: virtual void DoSettingsExchange(BOOL bSave); };

Ясно, что в переменной m_nNumber будут хранится числовые, в m_strString - текстовые, а в m_pDibData - бинарные данные. Переменная m_dwDibDataSize хранит объем бинарных данных. При чтении, ее значение будет устанавливаться исходя из объема считанных данных, при записи - в ней должно находится значение объема записываемых данных.

Реализация функции CFormSettings::DoSettingsExchange выглядит так:



void CFormSettings::DoSettingsExchange(BOOL bSave) { SX_MAP_Uint(bSave, "Number", m_nNumber); SX_MAP_Text(bSave, "String", m_strString); SX_MAP_Binary(bSave, "Binary", m_pDibData, m_dwDibDataSize); }

В методах CFormDialog::SaveSettings и CFormDialog::LoadSettings реализуем сохранение и загрузку параметров (с обработкой возможных ошибок и выдачей диагностических сообщений). Эти методы вызываются из обработчиков OnInitDialog и OnDestroy.



void CFormDialog::LoadSettings() { if(!m_sFormSettings.Load()) { CString strError; strError.Format(IDP_LOAD_SETTINGS_ERROR, (LPCTSTR) m_sFormSettings.GetErrorString()); AfxMessageBox((LPCTSTR) strError); } else { if(m_sFormSettings.IsVariableChanged(m_sFormSettings.m_pDibData)) { if((m_sFormSettings.m_pDibData != NULL) && (m_sFormSettings.m_dwDibDataSize != 0)) { // Загрузим картинку m_wndPaint.SetDIB(m_sFormSettings.m_pDibData); // Удалим данные ввиду их ненужности delete [] m_sFormSettings.m_pDibData; m_sFormSettings.m_pDibData = NULL; } } UpdateData(FALSE); } } void CFormDialog::SaveSettings() { if(UpdateData()) { HANDLE hDib = m_wndPaint.GetDIB(); if(hDib) { m_sFormSettings.m_dwDibDataSize = ::GlobalSize(hDib); m_sFormSettings.m_pDibData = (BYTE *) ::GlobalLock(hDib); } if(!m_sFormSettings.Save()) { CString strError; strError.Format(IDP_SAVE_SETTINGS_ERROR, (LPCTSTR) m_sFormSettings.GetErrorString()); AfxMessageBox((LPCTSTR) strError); } if(m_sFormSettings.m_pDibData != NULL) { ::GlobalUnlock(hDib); m_sFormSettings.m_pDibData = NULL; m_sFormSettings.m_dwDibDataSize = 0; } if(hDib != NULL) ::GlobalFree(hDib); } else AfxMessageBox(IDS_APP_ERROR, MB_OK | MB_ICONHAND); }

Как видите, применение разработанной библиотеки не составляет никакого труда. Все делается практически "само".

Что еще осталось здесь подчеркнуть, так это - пару интересных моментов, которые использовались здесь при работе с библиотекой.

Момент первый.
Так как, фактически, класс CFormSettings используется для сохранения данных элементов управления диалогового окна, то получать/устанавливать эти данные для элементов управления можно стандартным для классов диалогов MFC способом - через функцию DoDataExchange (просто передавая ссылки на переменные-члены объекта m_sFormSettings в функции DDX_*):



void CFormDialog::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); //{{AFX_DATA_MAP(CFormDialog) ... DDX_Text(pDX, IDC_EDIT_STRING, m_sFormSettings.m_strString); DDV_MaxChars(pDX, m_sFormSettings.m_strString, 32767); DDX_Text(pDX, IDC_EDIT_NUMBER, m_sFormSettings.m_nNumber); //}}AFX_DATA_MAP }

Момент второй.
Вы, наверно, заметили, что в методе CFormDialog::LoadSettings полученные бинарные данные удаляются после того, как на их основе создана картинка в элементе управления для рисования. Действительно - данные больше не нужны, и ничто не мешает их удалить. Только надо не забывать устанавливать в NULL соответствующий указатель. Точно так же, в методе CFormDialog::SaveSettings значение указателя m_pDibData устанавливается только на период сохранения данных.

И последнее.
Класс MFC CWinApp ориентирован на сохранение параметров конфигурации только либо в реестре, либо в ini-файле (это также уже упоминалось). Он содержит переменные-члены m_pszRegistryKey - для хранения имени подключа реестра, и m_pszProfileName - для хранения пути к конфигурационному файлу, но при вызове CWinApp::SetRegistryKey содержимое m_pszProfileName удаляется. Поэтому, перед вызовом SetRegistryKey в CSettingsDemoApp::InitInstance содержимое m_pszProfileName просто запоминается для дальнейшего использования.

Автор статьи : Вахтуров Виктор.

Скачать исходники библиотеки, а также демонстрационное приложение можно с соответствующей страницы сайта: библиотека работы с параметрами конфигурации приложений.

Дискуссионные листы компьютерной тематики (продолжение)


Вебстроительство. Форум !!!

В дискуссионном листе ведется обсуждение различных аспектов создания сайтов - написание скриптов, использование, настройка портальных движков, и многое другое. Присоединяйтесь !

Поисковые системы. Форум !!!

Этот дискуссионный лист посвящен обсуждению поиковых систем, методов индексации сайтов поисковиками, способам оптимизации сайта под поисковые системы.

Хостинг. Обзоры и обсуждения платного и бесплатного хостинга.

Вы ищете хостинг (платный, бесплатный) ? Хотите спросить совета в выборе ? Можете обсудить это здесь. Поделитесь советом, если знаете. Или узнайте больше. Все о хостинге.


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

http://subscribe.ru/
http://subscribe.ru/feedback/
Подписан адрес:
Код этой рассылки: comp.soft.prog.qandacpp
Отписаться

В избранное