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

Программирование на Visual С++

  Все выпуски  

Программирование на Visual С++ - No.41 (Парсер MSXML , Перетаскивание окна)


Служба Рассылок Subscribe.Ru проекта Citycat.Ru

ПРОГРАММИРОВАНИЕ НА VISUAL C++

Выпуск No. 41 от 22 апреля 2001 г.

Добрый день, уважаемые подписчики!

Ну что ж, похоже старт у проекта RSDN получился достаточно хороший, особенно если судить по вашим откликам. Большое спасибо всем, кто заглянул на сайт - я уверен, что вы не остались разочарованы. А тем, кто еще не видел RSDN, настоятельно рекомендую посмотреть - наверняка вы найдете что-то интересное для себя. К тому же теперь на сайте появилась полноценная поисковая система.

Как я и обещал, с этого выпуска рассылка начинает публиковать статьи из RSDN, касающиеся программирования на Visual C++. Но не надо думать, что рассылка будет вам бесполезной, если вы  регулярно читаете статьи на сайте. Рассылка будет экономить ваши усилия и ваше время; кроме того, некоторые статьи в рассылке будут появляться даже раньше, чем на сайте. 

/ / / / СТАТЬЯ / / / / / / / / / / / / / / / / / / / / / /

Использование парсера MSXML для работы с XML-документами

Демонстрационный проект XMLNodeExerciser

Парсер MSXML основывается на объектной модели документа XML (XML Document Object Model, XML DOM). Поэтому важно в первую очередь рассмотреть различные объекты, связанные с документом. Они приведены в таблице 1. Эти объекты позаимствованы прямо из спецификаций XML. MSXML предпренимает дополнительные усилия для стыковки объектов XML DOM с моделью COM. Благодаря этому достаточно просто установить, какому объекту модели XML DOM соответствует тот или иной COM-интерфейс MSXML. Например, IXMLDOMNode представляет DOM-объект Node (узел).

Объект DOM Назначение
DOMImplementation Объект, который можно запросить об уровне поддержки модели DOM
DocumentFragment Представляет часть дерева (хорошо подходит для операций Вырезать/Вставить)
Document Представляет узел верхнего уровня в дереве
NodeList Объект-итератор для доступа к узлам XML
Node Расширяет базовое понятие помеченного элемента (tagged element) в XML
NamedNodeMap Поддержка пространства имён и итерации для коллекций атрибутов
CharacterData Объект для манипулирования текстом
Attr Представляет атрибут(ы) элемента
Element Узел, представляющий элемент XML (удобен для доступа к атрибутам)
Text Представляет текст, содержащийся в элементе или атрибуте
CDATASection Используется для отключения разбора и валидации некоторых разделов XML
Notation Сожержит нотацию, расположенную в DTD (Document Type Definition, описание типов документа) или в схеме
Entity Представляет разобранную или неразобранную сущность
EntityReference Представляет узел, ссылающийся на некоторую сущность
ProcessingInstruction Представляет инструкцию обработки
Таблица 1. Объекты XML DOM и их использование

Иногда это может сбивать с толку, но объекты XML-документа могут быть (и часто бывают) полиморфными. Так, узел (Node) в то же самое время является элементом (Element). Это вносит путаницу, когда вы решаете, какой объект DOM требуется для совершения некоторого действия. Вы создаёте узлы, используя объект документа (Document), но если вам требуется добавить атрибуты к только что созданному узлу, вам придётся поработать с ним как с одним из элементов. Если в отношениях между объектами и действиями над ними и существует какая-то закономерность, мне пока не удалось открыть её в процессе каждодневной работы. Я постоянно обращаюсь к документации в MSDN, чтобы посмотреть, какой интерфейс предоставляет методы, нужные мне для решения той или иной задачи. Методы различных объектов логически сгруппированы, и, по-видимому, именно этот принцип (группировка логически связанных операций) был использован при проектировании DOM.

Таким образом, весь фокус состоит в том, чтобы получить у парсера MSXML нужный DOM-объект, реализацию которого предоставляет объект COM. Обычная последовательность действий подразумевает создание COM-объекта самого MSXML, у которого затем можно запросить (или получить каким-то другим способом) указатели на другие объекты XML DOM (которые в свою очередь тоже являются COM-объектами).

Демонстрационное приложение, использующее XML DOM

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

  • Загружает XML-файл с диска.
  • Отыскивает определённый узел и добавляет к нему дочерний узел.
  • Находит ещё один узел и отображает содержащийся в нём текст.
  • Сохраняет изменённый документ на диск.

Чтобы ещё больше упростить задачу, я жёстко "зашил" в программу имена XML-файлов и узлов. Понятно, что в реальном приложении вы вряд ли примените эту тактику. Но в нашем случае она имеет смысл, так как ещё больше упрощает код, связанный с использованием MSXML.

Как и во многих других случаях, я использовал в своём примере библиотеку ATL как удобную обёртку для всех операций, связанных с COM. Поэтому вы непременно увидете, как я использую объекты CComPtr и CComQIPtr. Для ровного счёта я добавил к ним также объекты CComBSTR и CComVariant. Если они вам не знакомы, просто запомните, что они являются шаблонами и сами заботятся о многих деталях, которые для наших целей несущественны. Для нас важно рассмотреть, каким образом искать узлы XML, добавлять новые узлы и отображать содержащийся в них текст.

Моё консольное приложение будет загружать XML-документ под названием xmldata.xml (предполагается, что он лежит в одном каталоге с исполняемым файлом), содержащий следующие данные:

<?xml version="1.0"?>
<xmldata>
   <xmlnode />
   <xmltext>Hello, World!</xmltext>
</xmldata>

Сначала мы будем искать узел xmlnode, и если найдём, добавим к нему новый узел (с атрибутом) в качестве дочернего. В результате получится документ следующего вида:

<?xml version="1.0"?>
<xmldata>
   <xmlnode>
      <xmlchildnode xml="fun" />
   </xmlnode>
   <xmltext>Hello, World!</xmltext>
</xmldata>

Далее мы напечатаем сообщение, содержащееся в узле xmltext ("Hello, World!"), и сохраним полученный документ в файл updatedxml.xml. После этого вы сможете посмотреть результаты, используя текстовый редактор или Internet Explorer 5.x. Давайте займёмся кодом.

Прежде всего приложение инициализирует библиотеку COM, а затем создаёт экземпляр парсера MSXML:

CComPtr<IXMLDOMDocument> spXMLDOM;
HRESULT hr = spXMLDOM.CoCreateInstance(__uuidof(DOMDocument));

if ( FAILED(hr) )
   throw "Unable to create XML parser object";
if ( spXMLDOM.p == NULL )
   throw "Unable to create XML parser object";

Если нам удалось создать экземпляр парсера, мы загружаем в него XML-документ:

VARIANT_BOOL bSuccess = false;
hr = spXMLDOM->load(CComVariant(L"xmldata.xml"), &bSuccess);

if ( FAILED(hr) )
   throw "Unable to load XML document into the parser";
if ( !bSuccess )
   throw "Unable to load XML document into the parser";

Поиск узла осуществляется через объект документа, поэтому мы используем IXMLDOMDocument::selectSingleNode() для обнаружения нужного узла по его имени. Есть и другие способы, но этот наиболее прост, в том случае, если вы точно знаете, какой узел вам требуется.

CComBSTR bstrSS(L"xmldata/xmlnode");
CComPtr<IXMLDOMNode> spXMLNode;
hr = spXMLDOM->selectSingleNode(bstrSS,&spXMLNode);

if ( FAILED(hr) )
   throw "Unable to locate 'xmlnode' XML node";
if ( spXMLNode.p == NULL )
   throw "Unable to locate 'xmlnode' XML node";

Другие методы, о которых вам следует знать, - это IXMLDOMDocument::nodeFromID() и IXMLDOMElement::getElementsByTagName(), которые вы можете использовать, чтобы получить список узлов в документе. Вы также можете обратиться к документу как к дереву и просканировать его (получая дочерний узел, все узлы одного уровня и т. д.).

В любом случае, результатом поиска станет объект узла MSXML, IXMLDOMNode. Узел должен существовать где-то в документе, иначе поиск закончится неудачей. Моё приложение использует его как родителя для совершенно нового узла, который создаётся объектом XML-документа:

CComPtr<IXMLDOMNode> spXMLChildNode;
hr = spXMLDOM->createNode(CComVariant(NODE_ELEMENT),
                          CComBSTR("xmlchildnode"),
                          NULL,
                          &spXMLChildNode);

if ( FAILED(hr) )
   throw "Unable to create 'xmlchildnode' XML node";
if ( spXMLChildNode.p == NULL )
   throw "Unable to create 'xmlchildnode' XML node";

Если парсеру удалось создать новый узел, следующий шаг - разместить его в дереве XML. Метод IXMLDOMNode::appendChild() - как раз то, что нам нужно.

CComPtr<IXMLDOMNode> spInsertedNode;
hr = spXMLNode->appendChild(spXMLChildNode,&spInsertedNode);

if ( FAILED(hr) )
   throw "Unable to move 'xmlchildnode' XML node";
if ( spInsertedNode.p == NULL )
   throw "Unable to move 'xmlchildnode' XML node";

Если родительский узел принял только что созданный узел в качестве дочернего, он вернёт вам ещё один экземпляр IXMLDOMNode, который представляет новый узел. На самом деле, этот новый узел и узел, который вы передали в appendChild(), в точности совпадают. Тем не менее, проверка указателя на добавленный дочерний узел может быть полезной, так как в случае ошибки он примет значение NULL.

Итак, мы уже нашли требуемый узел и добавили к нему дочерний узел; теперь посмотрим, как работать с атрибутами. Представьте себе, что вам нужно добавить к новому дочернему узлу атрибут:

xml="fun"

Сделать это не сложно, но вам придётся переключиться с IXMLDOMNode на IXMLDOMElement, чтобы поработать с узлом как с элементом. На практике это означает, что вам придётся запросить у интерфейса IXMLDOMNode связанный с ним интерфейс IXMLDOMElement, а потом, получив его, вызвать IXMLDOMElement::setAttribute():

CComQIPtr<IXMLDOMElement> spXMLChildElement;
spXMLChildElement = spInsertedNode;
if ( spXMLChildElement.p == NULL )
   throw "Unable to query for 'xmlchildnode' XML element interface";

hr = spXMLChildElement->setAttribute(CComBSTR(L"xml"),CComVariant(L"fun"));
if ( FAILED(hr) )
   throw "Unable to insert new attribute";

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

Для извлечение данных предназначен метод IXMLDOMNode::get_nodeTypedValue(). Данные, которые содержит узел, можно задавать с использованием схемы типов фирмы Microsoft, поэтому вы без труда можете сохранять числа с плавающей точкой, целые числа, строки или любые другие поддерживаемые схемой данные. Тип данных задаётся с использованием атрибута dt:type, например:

<model dt:type="string">SL-2</model>
<year dt:type="int">1992</year>

Если некоторый узел содержит данные заданного типа, вы сможете извлечь их в нужном формате, используя get_nodeTypedValue(). Если тип не задан, по умолчанию он считается текстовым, и парсер вернёт вам VARIANT с содержащимся в нём BSTR. В нашем случае этого достаточно, поскольку узел, который мы ищем, является текстовым и действительно содержит строку. Если нужно, мы всегда сможем отконвертировать её в другое представление, используя средства типа atoi(). А пока просто извлечём строку и отобразим её.

CComVariant varValue(VT_EMPTY);
hr = spXMLNode->get_nodeTypedValue(&varValue);
if ( FAILED(hr) )
   throw "Unable to retrieve 'xmltext' text";

if ( varValue.vt == VT_BSTR ) {
   // Display the results... since we're not using the
   // wide version of the STL, we need to convert the
   // BSTR to ANSI text for display...

   USES_CONVERSION;
   LPTSTR lpstrMsg = W2T(varValue.bstrVal);
   std::cout << lpstrMsg << std::endl;
}
else {
   // Some error
   throw "Unable to retrieve 'xmltext' text";
}

Если нам удалось извлечь значение, связанное с узлом, и если оно оказалось именно того типа, который мы ожидаем (BSTR), мы выводим текст на экран. В противном случае просто выводится сообщение об ошибке. Но вы, в зависимости от ситуации, можете предпринять и другие действия.

Наша последняя задача - сохранить обновлённое XML-дерево на диск, что мы и делаем, используя IXMLDOMDocument::save():

hr = spXMLDOM->save(CComVariant("updatedxml.xml"));
if ( FAILED(hr) )
   throw "Unable to save updated XML document";

Сохранив документ, программа выдаёт на экран короткое сообщение и завершается.

Эта демонстрационная программа вряд ли поразит ваше воображение. Вы могли бы сделать ещё очень много, но я надеюсь, что этот простой пример показал вам, как использовать MSXML в программах на языке C++. Сам по себе парсер - сложный продукт, и я настоятельно рекомендую вам использовать MSDN как справочное руководство по нему. Парсер предоставляет множество интерфейсов, каждый из которых обычно содержит большое количество методов. Несмотря на это, я широко использую парсер в своих проектах и теперь, поработав и поэкспериментировав с ним, нахожу его простым и удобным в использовании. Я надеюсь, что и вы найдёте ему, а также XML в целом, множество применений.

/ / / / ВОПРОС-ОТВЕТ / / / / / / / / / / / / / / / /

Как разрешить перетаскивание окна за любую точку?

Автор: Алексей Кирюшкин

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

Пример - приложение DragWin (диалоговое окошко, MFC) иллюстрирует два способа осуществить перемещение окна с захватом его не только за заголовок, но и за любую точку на клиентской области. Идея первого способа проста - при получении сообщения о перемещении мыши передвигаем наше окно в соответствии с новыми координатами. Второй способ поизящнее, и заключается в некотором "обмане" Windows, после которого она считает, что мышь находится над заголовоком окна, даже если реально это уже клиентсткая часть.

Способ 1

Реализован для главного окна приложения. Заключается в написании собственных обработчиков нажатия (WM_LBUTTONDOWN), перемещения (WM_MOUSEMOVE) и отпускания (WM_LBUTTONUP) левой кнопки мыши. Обработчики на данные события устанавливаются стандартным образом - через MFC ClassWizard.

void CDragWinDlg::OnLButtonDown(UINT nFlags, CPoint point)
{
    // выставим флажок - пошло перетаскивание
    m_bMoveWindow = TRUE;
    // все сообщения от мыши - к нашему окну, независимо от координат
    // чтобы мышь не улетала с окна при быстром движении
    SetCapture();
    // сохраняем координаты окна
    GetWindowRect(m_RectDlg);
    // сохраняем положение мышки внутри окна программы
    ClientToScreen(&point);
    m_MouseInDlg = point - m_RectDlg.TopLeft();
    // меняем курсор, чтоб веселее было тащить
    m_hCursor = m_hCursorDown;
    ::SetCursor(m_hCursor);

    // вызываем обработчик по умолчанию
    CDialog::OnLButtonDown(nFlags, point);
}

void CDragWinDlg::OnMouseMove(UINT nFlags, CPoint point)
{
    if (m_bMoveWindow) // надо тащить
    {
        // преобразуем координаты мыши в экранные
        // именно они нужны будут для SetWindowPos()
        ClientToScreen(&point);
        // двигаем окно в соответствии с новыми координатами мыши
        SetWindowPos(&wndTop,
            point.x - m_MouseInDlg.x, point.y - m_MouseInDlg.y,
            m_RectDlg.right - m_RectDlg.left,
            m_RectDlg.bottom - m_RectDlg.top,
            SWP_SHOWWINDOW);
        // поскольку обработчик по умолчанию все равно будет использовать
        // первоначальные параметры сообщения
        // обратное преобразование ScreenToClient(&point);
        // можно не вызывать
    }

    // вызываем обработчик по умолчанию
    CDialog::OnMouseMove(nFlags, point);
}

void CDragWinDlg::OnLButtonUp(UINT nFlags, CPoint point)
{
    // перетаскивание закончилось
    m_bMoveWindow = FALSE;

    // "отпускаем" мышку
    ReleaseCapture();

    // меняем курсор на исходный
    m_hCursor = m_hCursorUp;

    // вызываем обработчик по умолчанию
    CDialog::OnLButtonUp(nFlags, point);
}

BOOL CDragWinDlg::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
{
    // заменяем курсор на свой
    ::SetCursor(m_hCursor);

    return TRUE;
    // !!! было return CDialog::OnSetCursor(pWnd, nHitTest, message);
}

Замена курсора естесственно не является критичной для собственно перетаскивания, а добавлена исключительно для визуализации процесса захвата окошка.

Способ 2

Реализован для окна About этого же приложения. Заключается в замене обработчика события WM_NCHITTEST, которое информирует об области, над которой в данный момент находится мышка. Обработчик этого сообщения также можно добавить через MFC ClassWizard. Предварительно на закладке ClassInfo для класса CAboutDlg нужно установить для Message Filter значение Window.

Переписываем функцию - обработчик следующим образом:

UINT CAboutDlg::OnNcHitTest(CPoint point)
{
    UINT ret = CDialog::OnNcHitTest(point);

    // если обработчик по умолчанию говорит нам что мышка
    // над клиентской областью окна,  заменяем возвращаемое
    // значение на HTCAPTION - мышка над заголовком окна, 
    // а за заголовок перемещать окно можно!
    if (ret ==  HTCLIENT)
        return HTCAPTION;

    return ret;
}

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

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Если у вас есть вопрос по программированию, вы можете задать его одном из форумов на RSDN.

Это все на сегодня. Не забывайте заходить на RSDN. До встречи!

Алекс Jenter   jenter@rsdn.ru
Красноярск, 2001. Рассылка является частью Проекта RSDN

Предыдущие выпуски     Статистика рассылки


RLE Banner Network

http://subscribe.ru/
E-mail: ask@subscribe.ru

В избранное