HHCOLREG
Регистрация HTMLHELP коллекций
Опубликовано: 03.02.2003
Исправлено: 15.04.2009
Версия текста: 1.0
Исходные тексты (VC7) и демо-коллекция 36кБ
HHCOLREG.exe - Исполняемый файл v 1.0.8.1 26кБ
Зачем это надо?
HTMLHELP коллекция (текстовый файл определенного формата с расширением .col) позволяет объединять для просмотра и поиска несколько справочных CHM-файлов, что бывает очень удобно при написании модульных приложений, когда с каждым модулем одновременно создается отдельный справочный файл. Однако чтобы данную коллекцию можно было использовать, её нужно зарегистрировать – внести записи о коллекции в регистрационный файл hhcolreg.dat.
Использование программы
HHCOLREG.exe – консольная win-программа, работающая с параметрами командной строки. При запуске без параметров, или с неправильным набором параметров выводится краткая справка:
Программа hhcolreg.exe предназначена для регистрации файлов HTMLHelp коллекций (*.col) на компьютере пользователя.
Использование:
hhcolreg.exe [-u] MyCollection.col [путь к hhcolreg.dat]
-u ( или /u ) - разрегистрировать, если этого ключа нет, то зарегистрировать
MyCollection.col - файл новой коллекции
Если путь к hhcolreg.dat не задан явно, программа пытается найти его самостоятельно. Предполагается также, что в узле TitleString задано имя соответствующего chm файла.
|
Т.о. в командной строке должен быть, по крайней мере, один параметр - имя файла коллекции. Для разрегистрации коллекции нужно добавить ключ –u. Если есть необходимость, можно потренироваться на копии файла hhcolreg.dat, указав её местоположение в командной строке. Если программа не находит hhcolreg.dat, она его создаёт.
Перед внесением изменений в файл коллекции и hhcolreg.dat они сохраняются с добавлением расширения .orig.
ПРЕДУПРЕЖДЕНИЕ
.orig будут переписываться каждый раз при регистрации коллекции, т.ч. если вы зарегистрируете две коллекции подряд, копия исходного hhcolreg.dat будет утеряна окончательно.
|
CHM-файлы коллекции должны находится в одном каталоге с регистрируемым COL-файлом, их имена должны совпадать с параметрами, прописанными в TitleString-ах файла коллекции.
Работа программы проверялась в OC Windows 2000, Windows XP и Windows 98. Впрочем, для запуска программы под win98 придется установить MSXML3, а для просмотра коллекции обновить версию HTMLHELP.
Подробности реализации
Программа написана в среде VS7, главным образом из-за наличия в ATL7 класса CAtlRegExp. При компиляции использовались также Microsoft Platform SDK November 2001 и STLPort 4.5, если возникнут проблемы при компиляции, копайте в этом направлении. В части вывода сообщений об ошибках использован класс CErrCodeMsg от Игоря Вартанова.
Пример коллекции
Разбор полетов будет проводиться на примере регистрации следующей коллекции:
<XML>
<HTMLHelpCollection>
<masterchm value="=first"/>
<masterlangid value=1033/>
<samplelocation value=""/>
<collectionnum value=10032/>
<refcount value=0/>
<version value=0/>
<findmergedchms value=0/>
<Folders>
<Folder>
<TitleString value="Раздел 1"/>
<FolderOrder value=0/>
<Folder>
<TitleString value="Часть 1"/>
<FolderOrder value=0/>
<Folder>
<TitleString value="=first"/>
<FolderOrder value=0/>
<LangId value=1033/>
</Folder>
</Folder>
</Folder>
<Folder>
<TitleString value="=second"/>
<FolderOrder value=1/>
<LangId value=1033/>
</Folder>
</Folders>
</HTMLHelpCollection>
</XML>
|
first и second – это псевдонимы файлов, составляющих нашу коллекцию, по ним программа просмотра HTMLHELP получит из hhcolreg.dat полные пути и имена chm-файлов коллекции. Для упрощения реализации процесса регистрации принято, что эти псевдонимы должны совпадать с именами соответствующих chm-файлов, находящихся в одном каталоге с файлом коллекции, хотя в общем случае это не обязательно. “Раздел 1” и “Часть 1” – просто названия (на что указывает отсутствие знака ‘=’ ) дополнительных папочек, внутрь которой будет помещено содержимое first.chm.
На этом примере видно, что файл коллекции, как собственно и hhcolreg.dat представляет собой своеобразный “недоXML”, т.к. здесь в отличии от “честного” XML числовые значения атрибутов не заключены в кавычки и отсутствует ProcessingInstruction,
<?xml version="1.0" encoding="Windows-1251"?>
|
указывающая, что русские буквы - в кодировке Windows-1251. Возможны два варианта работы с такими файлами:
- Написать свой парсер для извлечения, записи и удаления данных
- Использовать какой-либо готовый XML-парсер, предварительно обработав файлы коллекции и hhcolreg.dat – расставив недостающие кавычки и добавив на время обработки ProcessingInstruction. Причем кавычки по окончании регистрации можно и не убирать, т.к. HTMLHELP продолжает работать и при наличии кавычек, а вот от ProcessingInstruction по окончании процесса регистрации придется избавиться. В hhcolreg.exe реализован именно этот путь с использованием MSXML3.
Поиск hhcolreg.dat
Если расположение файла hhcolreg.dat не задано в командной строке, программа пытается найти его самостоятельно. В зависимости от версии Windows возможны два варианта расположения hhcolreg.dat в системе: для семейства NT это каталог %COMMON_APPDATA%\Microsoft\Html Help\ (например C:\Documents and Settings\All Users\Application Data\Microsoft\HTML Help), а для windows 98 – каталог %WINDOWS%\Help (например c:\windows\help).
ПРИМЕЧАНИЕ
В системах семейства NT может одновременно использоваться два файла hhcolreg.dat – один в %COMMON_APPDATA%\Microsoft\Html Help\, а второй в %WINDOWS%\Help\. При попытке просмотреть коллекцию первым проверяется файл %COMMON_APPDATA%\Microsoft\Html Help\hhcolreg.dat, если коллекция с искомым номером будет там найдена, то на этом все и прекращается, если нет, то проверяется файл %WINDOWS%\Help\hhcolreg.dat. Таким образом, если немножко постараться и зарегистрировать часть файлов коллекции в одном файле, а часть – в другом (у меня такое получалось при добавлении своих разделов к MSDN от VS6), то при просмотре коллекции будут отображены только файлы, зарегистрированные в %COMMON_APPDATA%\Microsoft\Html Help\hhcolreg.dat.
|
Поиск hhcolreg.dat
if ( argc < ( 3 + shift ) )
{
TCHAR hhcolregpath[ _MAX_PATH ];
if ( ::IsWindowsNT() )
{
hr = ::SHGetFolderPath( NULL, CSIDL_COMMON_APPDATA, NULL, 0,
hhcolregpath );
if ( FAILED( hr ) )
{
throw CErrInfo( hr, "Поиск каталога %COMMON_APPDATA%:" );
}
else
{
sColRegFileName = "\\Microsoft\\HTML Help\\hhcolreg.dat";
}
}
else
{
hr = ::SHGetFolderPath( NULL, CSIDL_WINDOWS, NULL, 0,
hhcolregpath );
if ( FAILED( hr ) )
{
throw CErrInfo( hr, "Поиск каталога %WINDOWS%:" );
}
else
{
sColRegFileName = "\\Help\\hhcolreg.dat";
}
}
sColRegFileName = hhcolregpath + sColRegFileName;
}
else
{
sColRegFileName = argv[ 2 + shift ];
}
|
Если файл hhcolreg.dat не найден там, где он должен находиться (актуально для win98 и свежеустановленной winXP) программа его создаёт:
CreateNewHhcolregDat
void CreateNewHhcolregDat( string sFileName )
{
CString sMsg;
CString sFileContent;
sMsg.Format( "Создаём файл %s\n", sFileName.c_str() );
RusOut( sMsg );
ofstream destfile( sFileName.c_str(), ios::out );
if ( !destfile )
{
sMsg.Format( "Невозможно создать новый файл HHCOLREG.dat.\n" );
throw sMsg;
}
sFileContent = "<XML>\n"
" <HTMLHelpDocInfo>\n"
" <NextCollectionId value=\"10\"/>\n"
" <Collections>\n"
" </Collections>\n"
" <Locations>\n"
" </Locations>\n"
" <DocCompilations>\n"
" </DocCompilations>\n"
" </HTMLHelpDocInfo>\n"
"</XML>";
destfile << ( LPCTSTR ) sFileContent;
destfile.close();
if ( _access( sFileName.c_str(), 0 ) == -1 )
{
sMsg.Format( "Не смогли создать файл %s для регистрации "
"коллекции.\n", sFileName.c_str() );
throw sMsg;
}
sMsg.Format( " Файл %s успешно создан.\n", sFileName.c_str() );
RusOut( sMsg );
}
|
Предварительная обработка файлов
Перед началом работы программой сохраняются оригиналы файлов коллекции и hhcolreg.dat в файлах с добавлением расширения .orig, а также создаются временные файлы коллекции и hhcolreg.dat с добавлением расширения .xml. Если в процессе работы программы все действия по регистрации коллекции проходят успешно, эти файлы копируются на место первоначальных файлов коллекции и hhcolreg.dat, а временные файлы удаляются.
Теперь нужно подготовить файл коллекции и hhcolreg.dat для загрузки в XML-парсер. Для этого создается временный файл и в его начало записывается строка ProcessingInstruction. Содержимое исходного файла построчно обрабатывается с помощью регулярного выражения, обеспечивающего замену записей вида 'value=1' на 'value="1"' и также записывается во временный файл. Если обработка успешно завершается, оба файла, исходный и временный закрываются, а затем временный файл копируется на место исходного. Все действия по предобработке файлов вынесены в функцию RepairFileAsXML():
RepairFileAsXML
void RepairFileAsXML( string sFileName )
{
HRESULT hr;
CString sMsg;
CString sFileContent;
sMsg.Format( "Предобработка файла %s\n", sFileName.c_str() );
RusOut( sMsg );
ifstream srcfile( sFileName.c_str(), ios::in );
if ( !srcfile )
{
sMsg.Format( "Невозможно открыть файл %s для чтения.\n",
sFileName.c_str() );
throw sMsg;
}
ofstream destfile( ( sFileName + ".new" ).c_str(),
ios::out |
ios::trunc );
if ( !destfile )
{
sMsg.Format( "Невозможно открыть файл %s для записи.\n",
( sFileName + ".new" ).c_str() );
throw sMsg;
}
destfile << sProcInstruction.c_str();
char* buf;
srcfile.seekg( 0, ios::end );
DWORD fsize = srcfile.tellg();
srcfile.seekg( 0, ios::beg );
buf = new char[ fsize + 1 ];
CRxUtil<> myRxUtil;
sMsg.Format( " Идет преобразование. Ждите...\n" );
RusOut( sMsg );
while ( srcfile.tellg() < fsize )
{
srcfile.getline( buf, fsize + 1 );
sFileContent = buf;
hr = myRxUtil.s( sFileContent,
"value={[0-9]+}/", "value=\"$1\"/",
true, true, true );
if ( FAILED( hr ) )
{
sMsg.Format( "Преобразование файла к XML:\n" );
throw CErrInfo( hr, sMsg.GetBuffer( 0 ) );
}
destfile << ( LPCTSTR ) sFileContent << "\n";
}
delete[] buf;
buf = NULL;
srcfile.close();
destfile.close();
if ( !::CopyFile( ( sFileName + ".new" ).c_str(),
sFileName.c_str(), FALSE ) )
{
sMsg.Format( "Запись обработанного файла %s вместо %s:",
( sFileName + ".new" ).c_str(),
sFileName.c_str() );
throw CErrInfo( ::GetLastError(), sMsg.GetBuffer( 0 ) );
}
if ( !::DeleteFile( ( sFileName + ".new" ).c_str() ) )
{
sMsg.Format( "Удаление временного файла %s:",
( sFileName + ".new" ).c_str() );
throw CErrInfo( ::GetLastError(), sMsg.GetBuffer( 0 ) );
}
sMsg.Format( " Файл %s преобразован к XML.\n", sFileName.c_str() );
RusOut( sMsg );
}
|
Далее с нашими файлами можно работать, используя MSXML. Предварительно, для получения текстового сообщения по коду ошибки MSXML необходимо загрузить модуль, содержащий текстовые описания для кодов ошибок, возвращаемых MSXML:
hmMSXML3R = ::LoadLibraryEx( _T( "msxml3r.dll" ),
NULL,
LOAD_LIBRARY_AS_DATAFILE);
|
Загрузка файлов и получение указателя на интерфейс IXMLDOMDocument2 для каждого файла происходят в LoadNewDocument().
Узнаём номер для новой коллекции
В файле hhcolreg.dat, в узле XML/HTMLHelpDocInfo/NextCollectionId хранится номер для следующей регистрируемой коллекции. Наша задача – узнать его, вписать в файл регистрируемой коллекции, нарастить на 1 и сохранить увеличенный номер в hhcolreg.dat. Функция GetNextCollectionNumber() получает указатель на IXMLDOMDocument2 для hhcolreg.dat и возвращает номер новой коллекции в CComVariant:
GetNextCollectionNumber
CComVariant
GetNextCollectionNumber( CComPtr<IXMLDOMDocument2> spHHCOLREGDoc )
{
USES_CONVERSION;
CString sMsg;
CComVariant varNextColNum, varNewColNum;
int NewColNum = 0;
CComPtr<IXMLDOMNode> spNextColNumNode;
CComQIPtr<IXMLDOMElement> spNextColNumElement;
CComBSTR bstrNextColNumNode( L"XML/HTMLHelpDocInfo/NextCollectionId" );
HRESULT hr = spHHCOLREGDoc->selectSingleNode( bstrNextColNumNode,
&spNextColNumNode );
if ( FAILED( hr ) )
{
sMsg.Format( "Поиск узла \"%s\" в файле для регистрации:",
W2A( bstrNextColNumNode ) );
throw CErrInfo( hr, sMsg.GetBuffer( 0 ), hmMSXML3R );
}
if ( spNextColNumNode.p == NULL )
{
sMsg.Format( "Не найден узел \"%s\" в файле для регистрации\n",
W2A( bstrNextColNumNode ) );
throw sMsg;
}
spNextColNumElement = spNextColNumNode;
if ( spNextColNumElement.p == NULL )
{
sMsg.Format( "Ошибка при подключении к \"%s\" в файле для "
"регистрации\n",
W2A( bstrNextColNumNode ) );
throw sMsg;
}
hr = spNextColNumElement->getAttribute( CComBSTR( L"value" ),
&varNextColNum );
if ( FAILED( hr ) )
throw CErrInfo( hr, "Чтение номера коллекции:", hmMSXML3R );
if ( varNextColNum.vt == VT_BSTR )
{
NewColNum = atoi( W2A( varNextColNum.bstrVal ) );
}
else
{
throw "Ошибка: Тип номера коллекции не BSTR.\n";
}
varNewColNum = varNextColNum;
ATLTRACE( "NewColNum = %d\n", NewColNum );
sMsg.Format( "Считали номер следующей коллекции ( %d ) из файла для "
"регистрации\n",
NewColNum );
RusOut( sMsg );
sMsg.Format( "%d", NewColNum + 1 );
CComBSTR bstrNextColNum = A2W( sMsg.GetBuffer( 0 ) );
varNextColNum = bstrNextColNum;
hr = spNextColNumElement->setAttribute( CComBSTR( L"value" ),
varNextColNum );
if ( FAILED( hr ) )
{
sMsg.Format( "Установка номера коллекции (%s) в файле для "
"регистрации:",
W2A( bstrNextColNumNode ) );
throw CErrInfo( hr, sMsg.GetBuffer( 0 ), hmMSXML3R );
}
sMsg.Format( "Изменили номер следующей коллекции на %d\n",
NewColNum + 1 );
RusOut( sMsg );
return varNewColNum;
}
|
Теперь вписываем этот номер в файл новой коллекции, он должен хранится в узле XML/HTMLHelpCollection/collectionnum:
SetNewCollectionNumber
void SetNewCollectionNumber( CComPtr<IXMLDOMDocument2> spNewCollDoc,
CComVariant varNewNumber )
{
USES_CONVERSION;
CString sMsg;
HRESULT hr;
CComPtr<IXMLDOMNode> spColNumNode;
CComBSTR bstrColNumNode( L"XML/HTMLHelpCollection/collectionnum" );
hr = spNewCollDoc->selectSingleNode( bstrColNumNode, &spColNumNode );
if ( FAILED( hr ) )
{
sMsg.Format( "Поиск узла \"%s\" в файле новой коллекции:",
W2A( bstrColNumNode ) );
throw CErrInfo( hr, sMsg.GetBuffer( 0 ), hmMSXML3R );
}
if ( spColNumNode.p == NULL )
{
sMsg.Format( "Не найден узел \"%s\" в файле новой коллекции\n",
W2A( bstrColNumNode ) );
throw sMsg;
}
::SetNodeAttribute( spColNumNode, CComBSTR( L"value" ), varNewNumber );
sMsg.Format( "Установили номер новой коллекции в %s\n",
W2A ( varNewNumber.bstrVal ) );
RusOut( sMsg );
}
|
Формируем список файлов новой коллекции
Для формирования списка chm-файлов, входящих в коллекцию нужно обойти и проанализировать значение атрибута value всех узлов TitleString. Если это значение начинается со знака '=' в соответствии с соглашением, описанным в разделе "Пример коллекции" считаем его именем (без пути и расширения) одного из файлов коллекции и сохраняем в своем списке. Если знака '=' в первой позиции нет – это просто название одного из разделов коллекции.
На всякий случай повторю еще раз – то, что для данной программы-регистратора необходимо строгое соответствие псевдонима файла из TitleString коллекции его реальному имени – это не более чем частный случай, упрощающий нам реализацию процесса регистрации, т.к. иначе пришлось бы вводить и анализировать дополнительный файл с соответствиями псевдонимов файлов и их реальных имен. Прежде чем вы попытаетесь зарегистрировать какую-либо коллекцию, созданную не вами, убедитесь, что ее автор придерживался той же логики.
|
GetDocumentList
void GetDocumentsList( CComPtr<IXMLDOMDocument2> spNewCollDoc,
vector<string>& ColDocNames )
{
USES_CONVERSION;
CComPtr<IXMLDOMNodeList> pFolderList = NULL;
CComPtr<IXMLDOMNode> pNextFolder = NULL;
HRESULT hr = spNewCollDoc->getElementsByTagName(
CComBSTR( L"*/TitleString" ),
&pFolderList );
if ( FAILED( hr ) )
throw CErrInfo( hr, "Получение списка названий документов "
"коллекции:", hmMSXML3R );
if ( pFolderList == NULL )
throw "Не смогли получить список названий документов коллекции\n";
long FoldersCount = 0;
hr = pFolderList->get_length( &FoldersCount );
if ( FAILED( hr ) )
throw CErrInfo( hr, "Получение количества документов коллекции:",
hmMSXML3R );
pFolderList->reset();
CComQIPtr<IXMLDOMElement> spNextFolderElement;
CComVariant varTitleString;
string stmp;
for ( int ii = 0; ii < FoldersCount; ii++ )
{
hr = pFolderList->get_item( ii, &pNextFolder );
if ( FAILED( hr ) )
throw CErrInfo( hr, "Получение следующего документа:",
hmMSXML3R );
if ( pNextFolder )
{
spNextFolderElement = pNextFolder;
hr = spNextFolderElement->getAttribute( CComBSTR( L"value" ),
&varTitleString );
if ( FAILED( hr ) )
throw CErrInfo( hr, "Чтение названия документа:",
hmMSXML3R );
if ( varTitleString.vt == VT_BSTR )
{
stmp = W2A( varTitleString.bstrVal );
if ( stmp[ 0 ] == '=' )
{
stmp.erase( 0, 1 );
ColDocNames.push_back( stmp );
}
}
pNextFolder = NULL;
}
}
RusOut( "Прочитали список документов коллекции\n" );
}
|
Собственно регистрация
Очистка от предыдущих регистраций данной коллекции
Наличие нескольких регистраций одной и той же коллекции под разными номерами и из разных каталогов может быть одной из причин ошибки при попытке открыть коллекцию для просмотра (HTMLHELP сможет открыть для просмотра только первый из нескольких зарегистрированных экземпляров коллекции), поэтому нужно найти все предыдущие регистрации данной коллекции и удалить их.
Правда в этом случае HTMLHELP сможет открыть для просмотра только последний зарегистрированный экземпляр коллекции, но это всё же больше соответствует логике пользователя, который переписывает коллекцию на новое место, регистрирует её и пытается просмотреть.
|
Для очистки hhcolreg.dat от предыдущих регистраций вызывается функция разрегистрации, UnregisterCollection, принимающая в качестве параметра имя файла коллекции. Если имя файла (+ расширение) разрегистрируемой коллекции встречается в полном имени какой-либо из уже зарегистрированных в hhcolreg.dat коллекций, данные об этой коллекции и её документах удаляются из hhcolreg.dat. Сравнение имен файлов нужно проводить без учета регистра, т.к. предыдущие регистрации могли быть произведены другой программой, с приведением всех символов в имени файла к верхнему или нижнему регистру.
Итак, по очереди обходим в hhcolreg.dat все узлы XML/HTMLHelpDocInfo/Collections/Collection, в которых атрибут value дочернего узла ColName содержит имя файла разрегистрируемой коллекции. Для каждой найденной таким образом коллекции получаем ее номер из атрибута value соседнего с ColName узла ColNum. По каждому полученному номеру выбираем документы, входящие в коллекцию (для каждого документа номер его коллекции находится в атрибуте value узла XML/HTMLHelpDocInfo/DocCompilations/DocCompilation/LocationHistory/ColNum) и удаляем секцию XML/HTMLHelpDocInfo/DocCompilations/DocCompilation содержащую данные этого документа. На последнем этапе удаляются секции XML/HTMLHelpDocInfo/Collections/Collection всех попавших под нашу выборку коллекций.
UnregisterCollection
void UnregisterCollection(
CComPtr<IXMLDOMDocument2> spHHCOLREGDoc,
string sNewColFullName )
{
USES_CONVERSION;
CComPtr<IXMLDOMNodeList> pColNumFolderList = NULL;
CComPtr<IXMLDOMNodeList> pDocCompFolderList = NULL;
CComPtr<IXMLDOMNodeList> pCollectionFolderList = NULL;
CComPtr<IXMLDOMNode> pNextColNumFolder = NULL;
CComPtr<IXMLDOMNode> pNextDocCompFolder = NULL;
CComPtr<IXMLDOMNode> pNextCollectionFolder = NULL;
CComPtr<IXMLDOMNode> pRemovedFolder = NULL;
CComPtr<IXMLDOMNode> pParentDocCompilationsFolder = NULL;
CComPtr<IXMLDOMNode> pParentCollectionsFolder = NULL;
CComQIPtr<IXMLDOMElement> spColNumElement;
CComVariant varColNum;
CString sMsg, sPath;
HRESULT hr;
char drive[ _MAX_DRIVE ];
char dir[ _MAX_DIR ];
char fname[ _MAX_FNAME ];
char ext[ _MAX_EXT ];
string sColFileName;
_splitpath( sNewColFullName.c_str(), drive, dir, fname, ext );
sColFileName = fname;
sColFileName += ext;
hr = spHHCOLREGDoc->selectSingleNode(
CComBSTR( L"XML/HTMLHelpDocInfo/DocCompilations" ),
&pParentDocCompilationsFolder );
if ( FAILED( hr ) )
throw CErrInfo( hr, "Получение родительского узла "
"<DocCompilations>:\n", hmMSXML3R );
if ( pParentDocCompilationsFolder == NULL )
throw "Не найден узел <DocCompilations>\n";
hr = spHHCOLREGDoc->selectSingleNode(
CComBSTR( L"XML/HTMLHelpDocInfo/Collections" ),
&pParentCollectionsFolder );
if ( FAILED( hr ) )
throw CErrInfo( hr, "Получение родительского узла <Collections>:\n",
hmMSXML3R );
if ( pParentCollectionsFolder == NULL )
throw "Не найден узел <Collections>\n";
CString lowername = sColFileName.c_str();
lowername.MakeLower();
sPath.Format( "//*/Collection[ColName"
"[contains(translate(@value,'%s','%s'),'%s')]]/ColNum",
UpperChars, LowerChars, lowername );
hr = spHHCOLREGDoc->selectNodes( CComBSTR( sPath.GetBuffer( 0 ) ),
&pColNumFolderList );
if ( FAILED( hr ) )
throw CErrInfo( hr, "Получение списка узлов <ColNum>:", hmMSXML3R );
if ( pColNumFolderList == NULL )
throw "Не смогли получить список узлов <ColNum>\n";
long ColNumFoldersCount = 0;
long DocCompilationFoldersCount = 0;
hr = pColNumFolderList->get_length( &ColNumFoldersCount );
if ( FAILED( hr ) )
throw CErrInfo( hr, "Получение количества узлов <ColNum> c номерами "
"удаляемых коллекций:", hmMSXML3R );
pColNumFolderList->reset();
int i = 0;
int k = 0;
for ( i = 0; i < ColNumFoldersCount; i++ )
{
hr = pColNumFolderList->get_item( i, &pNextColNumFolder );
if ( FAILED( hr ) )
throw CErrInfo( hr, "Получение следующего узла ColNum из списка "
"удаляемых записей:", hmMSXML3R );
if ( pNextColNumFolder )
{
spColNumElement = pNextColNumFolder;
hr = spColNumElement->getAttribute( CComBSTR( L"value" ),
&varColNum );
if ( FAILED( hr ) )
throw CErrInfo( hr, "Чтение номера коллекции для удаления:",
hmMSXML3R );
sPath.Format( "//XML/HTMLHelpDocInfo/DocCompilations/"
"DocCompilation"
"[LocationHistory/ColNum[@value='%s']]",
W2A( varColNum.bstrVal ) );
hr = spHHCOLREGDoc->
selectNodes( CComBSTR( sPath.GetBuffer( 0 ) ),
&pDocCompFolderList );
if ( FAILED( hr ) )
throw CErrInfo( hr, "Получение списка узлов "
"<DocCompilation> для удаления:\n",
hmMSXML3R );
if ( pDocCompFolderList == NULL )
throw "Не смогли получить список узлов <DocCompilation> "
"для удаления\n";
DocCompilationFoldersCount = 0;
hr = pDocCompFolderList->
get_length( &DocCompilationFoldersCount );
if ( FAILED( hr ) )
throw CErrInfo( hr, "Получение количества узлов "
"<DocCompilation> для удаления:\n",
hmMSXML3R );
pDocCompFolderList->reset();
for ( k = 0; k < DocCompilationFoldersCount; k++ )
{
hr = pDocCompFolderList->get_item( k, &pNextDocCompFolder );
if ( FAILED( hr ) )
throw CErrInfo( hr, "Получение следующего узла "
"<DocCompilation> из списка для "
"удаления:\n", hmMSXML3R );
if ( pNextDocCompFolder )
{
hr = pParentDocCompilationsFolder->
removeChild( pNextDocCompFolder,
&pRemovedFolder );
if ( FAILED( hr ) )
{
sMsg.Format( "Удаление узла <DocCompilation> "
"коллекции %s:\n",
W2A( varColNum.bstrVal ) );
throw CErrInfo( hr, sMsg.GetBuffer( 0 ), hmMSXML3R );
}
sMsg.Format( "Удалён узел <DocCompilation> "
"коллекции %s\n",
W2A( varColNum.bstrVal ) );
RusOut( sMsg );
pRemovedFolder = NULL;
pNextDocCompFolder = NULL;
}
}
pDocCompFolderList = NULL;
}
}
pParentDocCompilationsFolder = NULL;
pColNumFolderList = NULL;
sPath.Format( "//*/Collection[ColName"
"[contains(translate(@value,'%s','%s'),'%s')]]",
UpperChars, LowerChars, lowername );
hr = spHHCOLREGDoc->selectNodes( CComBSTR( sPath.GetBuffer( 0 ) ),
&pCollectionFolderList );
if ( FAILED( hr ) )
throw CErrInfo( hr, "Получение списка узлов <Collection> "
"для удаления:", hmMSXML3R );
if ( pCollectionFolderList == NULL )
throw "Не смогли получить список узлов <Collection> "
"для удаления\n";
long CollectionFoldersCount = 0;
hr = pCollectionFolderList->get_length( &CollectionFoldersCount );
if ( FAILED( hr ) )
throw CErrInfo( hr, "Получение количества узлов <Collection> "
"для удаления:", hmMSXML3R );
pCollectionFolderList->reset();
for ( int ii = 0; ii < CollectionFoldersCount; ii++ )
{
hr = pCollectionFolderList->get_item( ii, &pNextCollectionFolder );
if ( FAILED( hr ) )
throw CErrInfo( hr, "Получение следующего узла из списка для "
"удаления:", hmMSXML3R );
if ( pNextCollectionFolder )
{
hr = pParentCollectionsFolder->removeChild(
pNextCollectionFolder,
&pRemovedFolder );
if ( FAILED( hr ) )
{
sMsg.Format( "Удаление узла <Collection> коллекции %s:\n",
sColFileName.c_str() );
throw CErrInfo( hr, sMsg.GetBuffer( 0 ), hmMSXML3R );
}
sMsg.Format( "Удалён узел <Collection> коллекции %s\n",
sColFileName.c_str() );
RusOut( sMsg );
pRemovedFolder = NULL;
pNextCollectionFolder = NULL;
}
}
pParentCollectionsFolder = NULL;
pCollectionFolderList = NULL;
}
|
Регистрация файлов коллекции
Во-первых, нужно занести в hhcolreg.dat информацию о каждом файле коллекции. Для приведенного выше примера коллекции нужно добавить примерно следующее:
<DocCompilation>
<DocCompId value="first"/>
<DocCompLanguage value="1033"/>
<LocationHistory>
<ColNum value="10032"/>
<TitleLocation value="E:\PROJECTS\VC7\hhcolreg\test\first.chm"/>
<IndexLocation value="E:\PROJECTS\VC7\hhcolreg\test\first.chm"/>
<QueryLocation value=""/>
<LocationRef value=""/>
<Version value="1"/>
<LastPromptedVersion value="0"/>
<TitleSampleLocation value=""/>
<TitleQueryLocation value=""/>
<SupportsMerge value="0"/>
</LocationHistory>
</DocCompilation>
<DocCompilation>
<DocCompId value="second"/>
<DocCompLanguage value="1033"/>
<LocationHistory>
<ColNum value="10032"/>
<TitleLocation value="E:\PROJECTS\VC7\hhcolreg\test\second.chm"/>
<IndexLocation value="E:\PROJECTS\VC7\hhcolreg\test\second.chm"/>
<QueryLocation value=""/>
<LocationRef value=""/>
<Version value="1"/>
<LastPromptedVersion value="0"/>
<TitleSampleLocation value=""/>
<TitleQueryLocation value=""/>
<SupportsMerge value="0"/>
</LocationHistory>
</DocCompilation>
|
Это происходит в функции RegisterDocuments(). Для получения полного пути к файлу каждого документа к имени, полученному при чтении списка документов добавляется расширение chm и путь. совпадающий с путем к файлу коллекции. Его можно получить либо из полного имени файла коллекции, переданного в командной строке, либо, если передано только имя – как путь к текущему каталогу.
RegisterDocuments
void RegisterDocuments(
CComPtr<IXMLDOMDocument2> spHHCOLREGDoc,
string sColDir,
vector<string>& ColDocNames,
CComVariant varNewNumber )
{
USES_CONVERSION;
CString sMsg;
HRESULT hr;
CComPtr<IXMLDOMNode> spDocCompilationsNode;
CComBSTR bstrDocCompilations( L"XML/HTMLHelpDocInfo/DocCompilations" );
hr = spHHCOLREGDoc->selectSingleNode( bstrDocCompilations,
&spDocCompilationsNode );
if ( FAILED( hr ) )
{
sMsg.Format( "Поиск узла \"%s\" в файле регистрации коллекций:",
W2A( bstrDocCompilations ) );
throw CErrInfo( hr, sMsg.GetBuffer( 0 ), hmMSXML3R );
}
if ( spDocCompilationsNode.p == NULL )
{
sMsg.Format( "Не найден узел \"%s\" в файле регистрации коллекций\n",
W2A( bstrDocCompilations ) );
throw sMsg;
}
CComPtr<IXMLDOMNode> spNewNode;
CComPtr<IXMLDOMNode> spLocHistNode;
CComPtr<IXMLDOMNode> spNewDocCompNode;
CComVariant NextColDocFileName;
sMsg.Format( "Добавляем информацию о документах коллекции...\n" );
RusOut( sMsg );
for ( UINT k = 0; k < ColDocNames.size(); k++ )
{
NextColDocFileName = ( sColDir + ColDocNames[ k ] + ".chm" ).c_str();
spNewDocCompNode = ::CreateNode( spHHCOLREGDoc, spDocCompilationsNode,
CComBSTR( L"DocCompilation" ) );
spNewNode = ::CreateNode( spHHCOLREGDoc, spNewDocCompNode,
CComBSTR( L"DocCompId" ) );
::SetNodeAttribute( spNewNode, CComBSTR( L"value" ),
CComVariant( ColDocNames[ k ].c_str() ) );
spNewNode = ::CreateNode( spHHCOLREGDoc, spNewDocCompNode,
CComBSTR( L"DocCompLanguage" ) );
::SetNodeAttribute( spNewNode, CComBSTR( L"value" ),
CComVariant( L"1033" ) );
spLocHistNode = ::CreateNode( spHHCOLREGDoc, spNewDocCompNode,
CComBSTR( L"LocationHistory" ) );
spNewNode = ::CreateNode( spHHCOLREGDoc, spLocHistNode,
CComBSTR( L"ColNum" ) );
::SetNodeAttribute( spNewNode, CComBSTR( L"value" ), varNewNumber );
spNewNode = ::CreateNode( spHHCOLREGDoc, spLocHistNode,
CComBSTR( L"TitleLocation" ) );
::SetNodeAttribute( spNewNode, CComBSTR( L"value" ),
NextColDocFileName );
spNewNode = ::CreateNode( spHHCOLREGDoc, spLocHistNode,
CComBSTR( L"IndexLocation" ) );
::SetNodeAttribute( spNewNode, CComBSTR( L"value" ),
NextColDocFileName );
spNewNode = ::CreateNode( spHHCOLREGDoc, spLocHistNode,
CComBSTR( L"QueryLocation" ) );
::SetNodeAttribute( spNewNode, CComBSTR( L"value" ),
CComVariant( L"" ) );
spNewNode = ::CreateNode( spHHCOLREGDoc, spLocHistNode,
CComBSTR( L"LocationRef" ) );
::SetNodeAttribute( spNewNode, CComBSTR( L"value" ),
CComVariant( L"" ) );
spNewNode = ::CreateNode( spHHCOLREGDoc, spLocHistNode,
CComBSTR( L"Version" ) );
::SetNodeAttribute( spNewNode, CComBSTR( L"value" ),
CComVariant( L"1" ) );
spNewNode = ::CreateNode( spHHCOLREGDoc, spLocHistNode,
CComBSTR( L"LastPromptedVersion" ) );
::SetNodeAttribute( spNewNode, CComBSTR( L"value" ),
CComVariant( L"0" ) );
spNewNode = ::CreateNode( spHHCOLREGDoc, spLocHistNode,
CComBSTR( L"TitleSampleLocation" ) );
::SetNodeAttribute( spNewNode, CComBSTR( L"value" ),
CComVariant( L"" ) );
spNewNode = ::CreateNode( spHHCOLREGDoc, spLocHistNode,
CComBSTR( L"TitleQueryLocation" ) );
::SetNodeAttribute( spNewNode, CComBSTR( L"value" ),
CComVariant( L"" ) );
spNewNode = ::CreateNode( spHHCOLREGDoc, spLocHistNode,
CComBSTR( L"SupportsMerge" ) );
::SetNodeAttribute( spNewNode, CComBSTR( L"value" ),
CComVariant( L"0" ) );
sMsg.Format( " %s\n",
( sColDir + ColDocNames[ k ] + ".chm" ).c_str() );
RusOut( sMsg );
}
}
|
ПРИМЕЧАНИЕ
В данном случае предполагается, что информация об индексах находится в том же chm файле, а не в отдельном chi.
|
Регистрация файла коллекции
И, во-вторых, занести информацию о файле коллекции:
AddNewCollectionNode
void AddNewCollectionNode(
CComPtr<IXMLDOMDocument2> spHHCOLREGDoc,
CComVariant varNewColFile,
CComVariant varNewNumber )
{
USES_CONVERSION;
CString sMsg, sPath;
HRESULT hr;
CComPtr<IXMLDOMNodeList> pFolderList = NULL;
sPath.Format( "//XML/HTMLHelpDocInfo/Collections/Collection"
"[ColNum[@value='%s']]",
W2A( varNewNumber.bstrVal ) );
hr = spHHCOLREGDoc->selectNodes( CComBSTR( sPath.GetBuffer( 0 ) ),
&pFolderList );
if ( FAILED( hr ) )
throw CErrInfo( hr, "Поиск существующего узла <Collection> перед "
"добавлением:", hmMSXML3R );
if ( pFolderList == NULL )
throw "Не смогли получить список узлов <Collection> перед "
"добавлением\n";
long FoldersCount = 0;
hr = pFolderList->get_length( &FoldersCount );
if ( FAILED( hr ) )
throw CErrInfo( hr, "Получение количества узлов <Collection> перед "
"добавлением:", hmMSXML3R );
pFolderList = NULL;
if ( FoldersCount > 0 )
{
sMsg.Format( "Для коллекции %s уже есть узел <Collection> в файле "
"регистрации.\n",
W2A( varNewNumber.bstrVal ) );
RusOut( sMsg );
return ;
}
CComBSTR bstrCollectionsNode( L"XML/HTMLHelpDocInfo/Collections" );
CComPtr<IXMLDOMNode> spCollectionsNode;
CComPtr<IXMLDOMNode> spNewCollectionNode;
CComPtr<IXMLDOMNode> spNewColNumNode;
CComPtr<IXMLDOMNode> spNewColNameNode;
hr = spHHCOLREGDoc->selectSingleNode( bstrCollectionsNode,
&spCollectionsNode );
if ( FAILED( hr ) )
{
sMsg.Format( "Поиск узла \"%s\" в файле для регистрации:",
W2A( bstrCollectionsNode ) );
throw CErrInfo( hr, sMsg.GetBuffer( 0 ), hmMSXML3R );
}
if ( spCollectionsNode.p == NULL )
{
sMsg.Format( "Не найден узел \"%s\" в файле для регистрации\n",
W2A( bstrCollectionsNode ) );
throw sMsg;
}
spNewCollectionNode = ::CreateNode( spHHCOLREGDoc, spCollectionsNode,
CComBSTR( L"Collection" ) );
spNewColNumNode = ::CreateNode( spHHCOLREGDoc, spNewCollectionNode,
CComBSTR( L"ColNum" ) );
spNewColNameNode = ::CreateNode( spHHCOLREGDoc, spNewCollectionNode,
CComBSTR( L"ColName" ) );
::SetNodeAttribute( spNewColNumNode, CComBSTR( L"value" ),
varNewNumber );
::SetNodeAttribute( spNewColNameNode, CComBSTR( L"value" ),
varNewColFile );
RusOut( "Добавили узел <Collection> для новой коллекции в файле "
"регистрации.\n" );
}
|
в результате добавляется секция вида:
<Collection>
<ColNum value="10032"/>
<ColName value="E:\PROJECTS\VC7\hhcolreg\test\sample.col"/>
</Collection>
|
Сохранение внесённых изменений
Пока все изменения проводились только в памяти, в XML-документах, представляющих наши файлы. Теперь нужно сохранить внесенные изменения на диске. Операция предельно простая, но иррациональное желание видеть при просмотре файла правильное XML-деревце, а не одну строку длиной в 300000 символов заставляет сделать пару финтов. Весь XML-документ выгружается в BSTR-строку, строка обрабатывается – между тэгами вставляются знаки перевода строки, после чего снова загружается в XML-документ. Поскольку в процессе этих преобразований неизбежно теряется ProcessingInstruction с данными о кодировке, восстанавливаем ее, после чего со спокойной душой и чистой совестью вызываем save() и сохраняем документ в файле. Мораль: красота – страшная сила :)
SaveDocument
void SaveDocument( CComPtr<IXMLDOMDocument2> spDOMDoc, string sFileName )
{
USES_CONVERSION;
CString sMsg, slocator;
VARIANT_BOOL bSuccess = VARIANT_FALSE;
sMsg.Format( "Структурируем документ %s...\n", sFileName.c_str() );
RusOut( sMsg );
CComBSTR bstrXML( L"" );
HRESULT hr = spDOMDoc->get_xml( &bstrXML );
if ( FAILED( hr ) )
{
throw CErrInfo( hr, "Сохраняем документ в строке BSTR:", hmMSXML3R );
}
CString sXml = W2A( bstrXML );
sXml.Replace( "><", ">\n<" );
bstrXML = sXml.GetBuffer( 0 );
hr = spDOMDoc->loadXML( bstrXML, &bSuccess );
if ( FAILED( hr ) )
{
throw CErrInfo( hr, "Загрузка измененного документа:", hmMSXML3R );
}
if ( !bSuccess )
{
CComPtr<IXMLDOMParseError> errPtr = NULL;
CComBSTR bstrReason( L"" ), bstrSrc( L"" );
long LineInFile = 0, errCode = 0, linePos = 0;
hr = spDOMDoc->get_parseError( &errPtr );
if ( FAILED( hr ) )
{
throw CErrInfo( hr,
"Получаем IXMLDOMParseError для анализа "
"ошибки:", hmMSXML3R );
}
errPtr->get_reason( &bstrReason );
errPtr->get_line( &LineInFile );
errPtr->get_errorCode( &errCode );
errPtr->get_linepos( &linePos );
errPtr->get_srcText( &bstrSrc );
slocator = "";
for ( int j = 1; j < linePos; j++ )
{
slocator += " ";
}
slocator += "^";
sMsg.Format( "Ошибка 0x%X в строке %d поз. %d файла "
"%s:\n\n%s\n%s\n%s\n",
errCode, LineInFile, linePos, sFileName.c_str(),
W2A( bstrSrc ), slocator, W2A( bstrReason ) );
errPtr = NULL;
throw sMsg;
}
CComPtr <IXMLDOMNode> instr;
hr = spDOMDoc->get_firstChild( &instr );
if ( FAILED( hr ) )
{
throw CErrInfo( hr, "Получение ProcessingInstruction:", hmMSXML3R );
}
DOMNodeType tp;
instr->get_nodeType( &tp );
if ( tp == NODE_PROCESSING_INSTRUCTION )
{
::RusOut( "Обнаружили остатки ProcessingInstruction, "
"будем восстанавливать.\n" );
CComPtr<IXMLDOMProcessingInstruction> pi = NULL;
hr = instr->QueryInterface( __uuidof( IXMLDOMProcessingInstruction ),
( void** ) & pi );
if ( FAILED( hr ) )
{
throw CErrInfo( hr, "Получение IXMLDOMProcessingInstruction:",
hmMSXML3R );
}
CComBSTR instType( L"xml" );
CComBSTR instData( L"version=\"1.0\" encoding=\"WINDOWS-1251\"" );
CComPtr<IXMLDOMProcessingInstruction> ptrInstr;
hr = spDOMDoc->createProcessingInstruction( instType, instData,
&ptrInstr );
if ( FAILED( hr ) )
{
throw CErrInfo( hr, "Создание новой ProcessingInstruction:",
hmMSXML3R );
}
CComPtr<IXMLDOMNode> oldInstr;
hr = spDOMDoc->replaceChild( ptrInstr, instr, &oldInstr );
if ( FAILED( hr ) )
{
throw CErrInfo( hr, "Замена ProcessingInstruction:", hmMSXML3R );
}
sMsg.Format( "Восстановлена ProcessingInstruction: %s",
sProcInstruction.c_str() );
RusOut( sMsg );
}
sMsg.Format( "Сохраняем изменения в файле %s...\n", sFileName.c_str() );
RusOut( sMsg );
hr = spDOMDoc->save( CComVariant( sFileName.c_str() ) );
if ( FAILED( hr ) )
{
sMsg.Format( "Сохранение измененного файла %s:", sFileName.c_str() );
throw CErrInfo( hr, sMsg.GetBuffer( 0 ), hmMSXML3R );
}
}
|
Пост-обработка файлов
Осталось только вычистить строку ProcessingInstruction из начала файлов. Для этой операции удобно использовать класс CFileMap для работы с проецируемыми в память файлами от Виталия Брусенцева. Проецируем обрабатываемый файл в память, сдвигаем то что следует сразу за ProcessingInstruction в начало файла, устанавливаем новый конец и сохраняем изменения.
PostFix
void PostFix( string sFileName )
{
CString sMsg;
sMsg.Format( "Последняя чистка %s от ProcessingInstruction.\n",
sFileName.c_str() );
::RusOut( sMsg );
UINT PISHIFT = ( UINT ) sProcInstruction.size() + 1;
CFileMap file2fix( sFileName.c_str(), true );
if ( !file2fix )
{
sMsg.Format( "PostFix() : Невозможно открыть файл %s.\n",
sFileName.c_str() );
throw sMsg;
}
memmove( file2fix.Base(),
file2fix.Base() + PISHIFT,
file2fix.Size() - PISHIFT );
if ( !file2fix.Close( file2fix.Size() - PISHIFT ) )
{
sMsg.Format( "PostFix() : Невозможно установить новый "
"размер для %s.\n",
sFileName.c_str() );
throw sMsg;
}
}
|
Уфф-ф, может зря я не начал писать свой парсер? Ладно, если уж мы благополучно добрались до этого момента, осталось только сохранить временные файлы под их настоящими именами и почистить за собой.
Зарегистрированную коллекцию можно открыть для просмотра двойным щелчком, либо из командной строки: hh.exe sample.col:
.
Разрегистрация
При разрегистрации коллекции нужно удалить из hhcolreg.dat все относящиеся к ней записи. Для этого просто вызывается уже известная вам UnregisterCollection.
Заключение
Утилита с таким же назначением входит в состав инсталлятора GhostInstaller, однако, на момент написания данной программы она не вполне корректно регистрировала некоторые виды файлов коллекций, что собственно и стало причиной появления данного опуса.
Эта статья опубликована в журнале
RSDN Magazine
#2. Информацию о журнале можно найти здесь