Kultura
Помогающий
Offline
|
|
« : 11-11-2008 15:18 » |
|
Изучаю MSHTML. Помогите разобраться. Вопрос № 1: Вот что я делаю: a) С помощью IWebBrowser2 перехожу по нужному адрессу, получаю интерфейс IHTMLDocument2 странички: b) Получаю коллекцию всех эллементов: c) Дальше возникает вопрос: в мсдн вызывают IHTMLElementCollection::item, что бы найти интересующий объект, но что за параметры они передают в этот метод мне не понятно. Вот ссылка на страничку в мсдн: ms-help://MS.VSCC.v90/MS.MSDNQTR.v90.en/ieext/workshop/browser/mshtml/tutorials/sink.htm их код: _variant_t varID("myID", VT_BSTR); // ??? _variant_t varIdx(0, VT_I4);
hr = pElemColl->item(varID, varIdx, &pElemDisp);
Итак вопрос, что должно быть вместо "myID", а так же где смотреть список всех обьектов коллекции, как отыскать нужный объект? Вопрос № 2: Получив интерфейс объекта коллекции, мне нужно обрабатывать связанные с ним события. В мсдн на той же странице это делается с помощью точки соединения в предпоследнем примере: IConnectionPoint* pCP = NULL; <...> // pUnk is the IUnknown interface pointer for your event sink hr = pCP->Advise(pUnk, &dwCookie);
Недопонимаю, что такое pUnk в этом примере. Впрочем я методом тыка сделал свой обработчик сообщений, и вместо pUnk ставлю экземпляр этого класса. Вопрос: это будет работать? Если нет, то что надо ставить вместо pUnk? Кстати вот мой класс - обработчик: class EventSink : public IDispatch { public:
STDMETHOD (Invoke) (<...>) { switch (dispidMember) { <...> } return S_OK; }
/* IUnknown virtuals */ HRESULT STDMETHODCALLTYPE QueryInterface (<...>) {} ULONG STDMETHODCALLTYPE AddRef () {} ULONG STDMETHODCALLTYPE Release () {}
/* IDispatch virtuals */ HRESULT STDMETHODCALLTYPE GetTypeInfoCount (<...>) {} HRESULT STDMETHODCALLTYPE GetTypeInfo (<...>) {} HRESULT STDMETHODCALLTYPE GetIDsOfNames(<...>) {} };
Вопрос № 3: Как взаимодействовать со страницей? Например, перейти по ссылке или заполнить форму.
|
|
|
Записан
|
|
|
|
zubr
Гость
|
|
« Ответ #1 : 11-11-2008 17:38 » |
|
1. Если ты элементы будешь перечислять по именам, то тогда параметр varID типа VARIANT, будет объявлен примерно так: VARIANT var_id; var_id.vt = VT_BSTR; var_id.bstrVal = "ElName";
Если же перечисление по индексу: var_id.vt = VT_I4; for (int i = 0; i < lenHTMLElementCollection; i++) { pHtmlElDispatch = NULL; pHtmlEl = NULL; sourceInd = -1;
var.intVal = i; if ((pHTMLElementCollection->item(var_id, var_id, &pHtmlElDispatch) != S_OK) || (pHtmlElDispatch == NULL)) continue; if ((pHtmlElDispatch->QueryInterface (IID_IHTMLElement, (void**)&pHtmlEl) !=S_OK) || (pHtmlEl == NULL)) continue; ..............................
2. pUnk - это объект интерфейса HTML-элемента или HTML-документа или IWebBrowser, события которого ты планируешь перехватывать. 3. Перейти по ссылке - у IWebBrowser2 есть метод Navigate, заполнить форму - у IHTMLElement есть методы getAttribute, setAttribute получения, установка значения элемента формы, метод Click - имитация нажатия на элемент, к примеру нажать на Submit
|
|
|
Записан
|
|
|
|
Kultura
Помогающий
Offline
|
|
« Ответ #2 : 11-11-2008 20:09 » |
|
zubr, с 1-м и 3-м понятно, спасибо. Насчет второго, 2. pUnk - это объект интерфейса HTML-элемента или HTML-документа или IWebBrowser, события которого ты планируешь перехватывать.
к сожалению, теперь мне и вовсе не понятно, как происходит перехват . Все таки вернусь к тому самому примеру из мсдн, где у меня и возникли трудности. Если сократить: void CMyClass::ConnectEvents(IHTMLElement * pElem) { <...> 1) hr = pElem->QueryInterface(IID_IConnectionPointContainer, (void**)&pCPC); <...> 2) hr = pCPC->FindConnectionPoint(DIID_HTMLElementEvents2, &pCP); <...> 3) hr = pCP->Advise(pUnk, &dwCookie);
1) Получили интерфейс типа IID_IConnectionPointContainer для объекта IHTMLElement 2) Получили точку соприкосновения типа IConnectionPoint для интерфейса типа DIID_HTMLElementEvents2 3) Соединили pUnk - с интерфейсом DIID_HTMLElementEvents2. Насколько верно я вник в то, что здесь происходит? Тогда для данного примера pUnk - это pElem ? И еще вопрос, где и каким образом коннектится класс обработчик? Кстати, как он должен выглядеть тогда, например мой из первого поста (class EventSink : public IDispatch {<...>}) годится?
|
|
|
Записан
|
|
|
|
zubr
Гость
|
|
« Ответ #3 : 12-11-2008 04:50 » |
|
Извини, объект интерфейса HTML-элемента или HTML-документа или IWebBrowser - это неправильно (уже подзабыл). pUnk - это объект класса, наследующего IDispatch и реализующего метод Invoke с обработчиками событий. 1) Получили интерфейс типа IID_IConnectionPointContainer для объекта IHTMLElement 2) Получили точку соприкосновения типа IConnectionPoint для интерфейса типа DIID_HTMLElementEvents2 3) Соединили pUnk - с интерфейсом DIID_HTMLElementEvents2.
Правильно. Можешь сделать твой класс CMyClass наследуемым от IDispatch и в нем реализовать метод Invoke, тогда подключение к DIID_HTMLElementEvents2 будет выглядеть так: hr = pCP->Advise((IDispatch*)this, &dwCookie);
|
|
|
Записан
|
|
|
|
Kultura
Помогающий
Offline
|
|
« Ответ #4 : 12-11-2008 09:31 » |
|
С навигацией по коллекции все равно проблемы, если делаю так: VARIANT var_id; var_id.vt = VT_BSTR; var_id.bstrVal = "ElName";
ругается error C2440: '=' : cannot convert from 'const char [7]' to 'BSTR' Кстати, как имена объектов узнать, наверное, они стандартные , вроде "button" и т.д.? --------------------------------------------------------------------------------------------------------------------- Попробовал перебрать все эллементы коллекции и прикрепить к каждому свой обработчик, что-то не выходит. Вроде бы все выполняется без ошибок, но в саму функцию invoke не заходит. Вот мой обработчик: class EventSink : public IDispatch { public:
STDMETHOD (Invoke) (DISPID dispidMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS* pdispparams, VARIANT* pvarResult, EXCEPINFO* pexcepinfo, UINT* puArgErr) { switch (dispidMember) { default: /* any event */ break; }
return S_OK; }
/* IUnknown virtuals */ HRESULT STDMETHODCALLTYPE QueryInterface (REFIID riid, __RPC__deref_out void __RPC_FAR *__RPC_FAR *ppvObject) {return S_OK;} ULONG STDMETHODCALLTYPE AddRef () {return S_OK;} ULONG STDMETHODCALLTYPE Release () {return S_OK;}
/* IDispatch virtuals */ HRESULT STDMETHODCALLTYPE GetTypeInfoCount (__RPC__out UINT *pctinfo) {return S_OK;} HRESULT STDMETHODCALLTYPE GetTypeInfo (UINT iTInfo, LCID lcid, __RPC__deref_out_opt ITypeInfo **ppTInfo) {return S_OK;} HRESULT STDMETHODCALLTYPE GetIDsOfNames(__RPC__in REFIID riid, __RPC__in_ecount_full(cNames) LPOLESTR *rgszNames, UINT cNames, LCID lcid, __RPC__out_ecount_full(cNames) DISPID *rgDispId) {return S_OK;} };
Т.е. я просто ставлю точку остановки в теле функции Invoke и запускаю отладчик. Прикручиваю объект этого класса ко всем эллементам коллекции, с помощью той же функции из мсдн: void CMyClass::ConnectEvents(IHTMLElement * pElem) { <...> 1) hr = pElem->QueryInterface(IID_IConnectionPointContainer, (void**)&pCPC); <...> 2) hr = pCPC->FindConnectionPoint(DIID_HTMLElementEvents2, &pCP); <...> 3) hr = pCP->Advise(pUnk, &dwCookie); }
В ней pUnk = CMyClass:sink (указатель на объект EventSink-а). Далее нажимаю кнопочки на странице, но в тело Invoke не попадаю. Наверное, сам обработчик неправильно сделал?
|
|
|
Записан
|
|
|
|
zubr
Гость
|
|
« Ответ #5 : 12-11-2008 15:58 » |
|
Да, для BSTR надо вызывать SysAllocString, то есть: var_id.bstrVal = ::SysAllocString((OLECHAR*)"ElName"); ........... //после необходимых действий необходимо освободить строку ::SysFreeString(var_id.bstrVal);
Кстати, как имена объектов узнать, наверное, они стандартные , вроде "button" и т.д.? button - это тип элемента (тег), а имя - это значение атрибута Name элемента. Данный атрибут не обязательный, поэтому перечисление по именам реально только для форм, при условии, что элементы формы имеют имена, к примеру FirstName, LastName, Password Попробовал перебрать все эллементы коллекции и прикрепить к каждому свой обработчик, что-то не выходит. Вроде бы все выполняется без ошибок, но в саму функцию invoke не заходит. Кликать на элементах пробовал? Советую на ресурсе www.codeproject.com поискать код по ключевым словам: FindConnectionPoint, IID_IConnectionPointContainer, DIID_HTMLElementEvents2, Advise. Уверен, найдешь нужные тебе примеры. Ну, если не найдешь, сообщи, я пороюсь в своем архиве, наскребу тебе примеры кода.
|
|
|
Записан
|
|
|
|
Kultura
Помогающий
Offline
|
|
« Ответ #6 : 12-11-2008 19:32 » |
|
Да, отыскал на www.codeproject.com много подходящего. Пойду осваивать заморский ресурс =)
|
|
|
Записан
|
|
|
|
Kultura
Помогающий
Offline
|
|
« Ответ #7 : 13-11-2008 14:29 » |
|
Отыскал я себе подходящий код, вот отсюда: http://www.codeproject.com/KB/COM/TEventHandler.aspxРазобрался со статьей и примером, даже переделал для себя класс - обработчик. И заработало! Но не до конца, смотрю, смотрю, не могу понять в чем проблема. Не находится точка соединения: <...> device_event_interface = HTMLElementEvents2 IConnectionPoint* m_pIConnectionPoint; <...> void SetupConnectionPoint(device_interface* pdevice_interface) { IConnectionPointContainer* pIConnectionPointContainerTemp = NULL; IUnknown* pIUnknown = NULL; this -> QueryInterface(IID_IUnknown, (void**)&pIUnknown);
if (pIUnknown) { HRESULT hr = pdevice_interface -> QueryInterface (IID_IConnectionPointContainer, (void**)&pIConnectionPointContainerTemp); if (SUCCEEDED (hr)) { hr = pIConnectionPointContainerTemp -> FindConnectionPoint(__uuidof(device_event_interface), &m_pIConnectionPoint); pIConnectionPointContainerTemp -> Release(); pIConnectionPointContainerTemp = NULL; } if (m_pIConnectionPoint) { m_pIConnectionPoint -> Advise(pIUnknown, &m_dwEventCookie); } pIUnknown -> Release(); pIUnknown = NULL; } }
pIConnectionPointContainerTemp -> FindConnectionPoint(__uuidof(device_event_interface), &m_pIConnectionPoint); возвращает E_NOINTERFACE. До этого момента вроде все работает. pdevice_interface - это объект IHTMLElement. Я открываю "google.ru" и отыскиваю там радио - кнопку "Поиск в Интернете". Вот как я это делаю: void PeonWebInterface::GetIHTMLElement () { IDispatch * pElemDisp = NULL;
VARIANT var_id; var_id.vt = VT_BSTR; var_id.bstrVal = SysAllocString((OLECHAR *) L"lr"); VARIANT var_idx; var_idx.vt = VT_I4; var_idx.bstrVal = 0;
HRESULT hr = m_pElemColl->item (var_id, var_idx, & pElemDisp); if (SUCCEEDED (hr)) { hr = pElemDisp->QueryInterface (IID_IHTMLElement, (void**)& m_pElem); if (SUCCEEDED (hr)) { // Obtained element with ID of "myID". m_pIHTMLElementEventHandler = new IHTMLElementEventHandler (* this, m_pElem, & PeonWebInterface::OnIHTMLElementInvoke);
------------------------- этот конструктор вызывает SetupConnectionPoint (m_pElem) ------------------------------
} pElemDisp->Release (); SysFreeString (var_id.bstrVal); }
Если по приведенному фрагменту есть возможность обнаружить ошибку, помогите, пожалуйста
|
|
|
Записан
|
|
|
|
zubr
Гость
|
|
« Ответ #8 : 13-11-2008 17:43 » |
|
1. А что у тебя в pdevice_interface? 2. Я бы делал чтобы pdevice_interface был объект документа IHTMLDocument2, а в качестве интерфейса событий соответственно HTMLDocumentEvents2. Тогда, к примеру в событии OnClick элемента VARIANT_BOOL onclick(IHTMLEventObj *pEvtObj), легко определить в каком элементе произошел клик (см. интерфейс IHTMLEventObj::srcElement).
|
|
|
Записан
|
|
|
|
Kultura
Помогающий
Offline
|
|
« Ответ #9 : 13-11-2008 18:41 » |
|
1. А что у тебя в pdevice_interface?
Туда кидаю объект IHTMLElement, полученный из объекта IHTMLElementCollection. Я считываю коллекцию со странички http://www.google.ru/. Посмотри, правильно я нахожу , например кнопку "поиск в интернете". Ее же и кидаю в качестве pdevice_interface потом: IDispatch * pElemDisp = NULL;
VARIANT var_id; var_id.vt = VT_BSTR; var_id.bstrVal = SysAllocString((OLECHAR *) L"lr"); VARIANT var_idx; var_idx.vt = VT_I4; var_idx.bstrVal = 0;
HRESULT hr = m_pElemColl->item (var_id, var_idx, & pElemDisp); if (SUCCEEDED (hr)) { hr = pElemDisp->QueryInterface (IID_IHTMLElement, (void**)& m_pElem); if (SUCCEEDED (hr)) { // Obtained element <...>
2. Я бы делал чтобы pdevice_interface был объект документа IHTMLDocument2...
Завтра с утра попробую так сделать.
|
|
|
Записан
|
|
|
|
zubr
Гость
|
|
« Ответ #10 : 14-11-2008 05:04 » |
|
Kultura, извини, но изучать указанную тобой страницу на предмет имен элементов нет времени (тяну 2 проекта). Совет, перечисли все элементы по номерам в коллекции (как я тебе показывал), получай их атрибуты (name, tegName, value), смотри все это в отладке или выдавай отладочные сообщения. Ну а затем, определив номер интересующего тебя элемента получай его интерфейс по номеру (так будет надежнее).
|
|
|
Записан
|
|
|
|
Kultura
Помогающий
Offline
|
|
« Ответ #11 : 14-11-2008 11:40 » |
|
zubr, все, я разобрался. Сделал по твоему совету: 2. Я бы делал чтобы pdevice_interface был объект документа IHTMLDocument2...
И все заработало. Спасибо! Проблема была в том, что я пытался соединяться к объектам, которые не поддерживают соединения. Вот и выскакивал E_NOINTERFACE.
|
|
|
Записан
|
|
|
|
Kultura
Помогающий
Offline
|
|
« Ответ #12 : 14-11-2008 17:51 » |
|
Сделаю небольшой отчет прогресса в MSHTML : теперь моя программа умеет ходить по ссылкам, реагировать на некоторые действия пользователя и находить нужный эллементы на страничке Вот, сейчас разбираюсь с событиями подробно. И не могу понять, как пользоваться событийными интерфейсами. Вот, например HTMLDocumentEvents2 имеет всякие методы, которые выглядят как функции, но как их вызывать и для чего . Очевидно, что я чего-то не понимаю, zubr, помоги разобраться. Если кто-то еще прочитает, объясните, пожалуйста. Наследовать и замещать? Из мсдн я так понял, методы эти вызываются системой, но многие из них объявлены в документации как void. Например, void HTMLDocumentEvents2::onkeydown(IHTMLEventObj *pEvtObj);
|
|
« Последнее редактирование: 14-11-2008 17:55 от Kultura »
|
Записан
|
|
|
|
zubr
Гость
|
|
« Ответ #13 : 14-11-2008 21:06 » |
|
1. Создать свой метод с подобным прототипом void CMyClass::onkeydown(IHTMLEventObj *pEvtObj); 2. Реализация данного метода и будет обработчиком события. 2. Вставить этот метод в метод Invoke: STDMETHODIMP CMyClass::Invoke(DISPID dispidMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS* pDispParams, VARIANT* pvarResult, EXCEPINFO* pExcepInfo, UINT* puArgErr) { switch (dispidMember) { case DISPID_HTMLDOCUMENTEVENTS2_ONKEYDOWN: onkeydown((IHTMLEventObj*)pDispParams->rgvarg[0].pdispVal); break; } }
|
|
|
Записан
|
|
|
|
Kultura
Помогающий
Offline
|
|
« Ответ #14 : 17-11-2008 17:24 » |
|
zubrу, и всем, кто читал эту тему привет и здравствовать, а я вернулся с новыми вопросами по MSHTML 1) Выполняю цепочку действий, состоящую в том числе из нескольких переходов по ссылкам в одной функции. Но перед каждым шагом нужно дождаться полной загрузки документа. IWebBrowser2::Navigate (...) (которым я хожу по ссылкам) ведет себя странно. После выполнения этой функции, IE переходит только по последней ссылке. Пробовал применять потоки и события по всякому, не получилось Например, если поместить Navigate (...) в отдельный поток и ждать, поток повисает и т.д. Вопрос: как сделать правильно? 2) Т.к. изучаю материал, пытаясь написать бота для браузерной игры, этот вопрос ну не мог не возникнуть Для некоторых действий используется flash. Вопрос: что это за зверь (где искать лит-ру) и как с ним работать?
|
|
|
Записан
|
|
|
|
zubr
Гость
|
|
« Ответ #15 : 17-11-2008 18:17 » |
|
1. Надо дождаться загрузки документа. Для этого можно воспользоваться событием IWebBrowser DocumentComplete (см. DWebBrowserEvents2). Или периодически проверять get_ReadyState у IWebBrowser - должно быть READYSTATE_COMPLETE. Правда и в том и другом случае есть нюансы при загрузке фреймов. Но пока реализуй без учета этих нюансов. 2. flash - это ActiveX-компонент, встраиваемый в броузер и позволяющий проигрывать файлы специального swf-формата, внутрь которого прописываются картинки, сценарии. Сам flash-плеер имеет интерактивные функции и возможности отображения векторной графики, поэтому swf-файлы позволяют создавать ограниченные по функционалу (функциями самого флеш-плеера) приложения. Если хочешь глубоко постичь данную тему, то наверно стоит начать с формата swf-файла. Вроде на сайте Adobe есть SDK по swf. Ну а по принципам создания флеш-приложений полно литературы в сети, думаю в гугл много ссылок найдешь. Также посмотри книжные магазины.
|
|
|
Записан
|
|
|
|
Kultura
Помогающий
Offline
|
|
« Ответ #16 : 17-11-2008 20:00 » |
|
Ну да, я использую DocumentComplete, знаю, когда загрузка завершена. Но цепочка вызовов Navigate в одной функции (например, 1.ru -> 2.ru -> 3.ru) ведет себя так: 3.ru -> DocumentComplete -> можно тыкать в IE. Т.е. она не ведет себя так: 1.ru -> DocumentComplete -> 2.ru -> DocumentComplete -> 3.ru -> DocumentComplete. Я думал затормозить цепочку, поставив после Navigate (х.ru) WaitForSingleObject, но тогда просто все повисает. Т.к. DocumentComplete не вызывается (а в нем у меня SetEvent), он вызывается только когда функция, вызвавшая Navigate прекратила работу. Вот сейчас пробую Navigate запускать в отдельном потоке. Результат выполнения HRESULT hr = pbrowser->Navigate (...) : в hr "приложение обратилось к интерфейсу, принадлежащему другому потоку". Пробовать разделять IWebBrowser2 * pbrowser, а как?
|
|
|
Записан
|
|
|
|
zubr
Гость
|
|
« Ответ #17 : 17-11-2008 21:51 » |
|
Navigate вызывай из DocumentComplete. Для этого создай в окне твоего приложения обработчик пользовательского сообщения, в котором и вызывай Navigate. А из DocumentComplete с помощью PostMessage посылай окну пользовательское сообщение. Не нужно никаких потоков. Если на странице есть фреймы, то DocumentComplete будет вызываться при загрузке каждого фрейма, чтобы убедиться, что загрузилась страница, а не очередной фрейм, сравниваем указатель IWebBrowser*, который вызывал Navigate и параметр pDisp события DocumentComplete, когда они равны - загрузилась страница.
|
|
|
Записан
|
|
|
|
Kultura
Помогающий
Offline
|
|
« Ответ #18 : 18-11-2008 16:06 » |
|
zubr, спасибо, проблема решена. Уже почти добрался уже до картинки, которую буду распознавать. Тут первая проблема jpg конвертировать в bmp. Подскажите?
|
|
|
Записан
|
|
|
|
zubr
Гость
|
|
« Ответ #19 : 18-11-2008 17:41 » |
|
Смотри класс Image из GDI+ (msdn). Есть и примеры: "Converting a BMP Image to a PNG Image"
|
|
|
Записан
|
|
|
|
Kultura
Помогающий
Offline
|
|
« Ответ #20 : 28-11-2008 11:58 » |
|
Добрался до картинки. Задача - взять ее со странички. Ну, и у меня не получается . Делаю так: IHTMLElementRender * pRender = NULL; HRESULT hr = pimageElem->QueryInterface(IID_IHTMLElementRender, (void **) &pRender); if (SUCCEEDED (hr)) { HDC htempDC; BITMAP bmImage; HBITMAP hbmImage; pRender->DrawToDC (htempDC); GetObject (hbmImage, sizeof (BITMAP), (LPVOID) & bmImage); }
QueryInterface (IID_IHTMLElementRender, (void **) &pRender) ругается E_NOINTERFACE. Перебрал всех родителей обьекта картинки, для всех выдает эту ошибку. Как сдалать правильно?
|
|
|
Записан
|
|
|
|
zubr
Гость
|
|
« Ответ #21 : 28-11-2008 18:49 » |
|
Ну можно попробовать IViewObject::Draw. IViewObject получать из документа через QueryInterface. Правда не всегда эти интерфейсы работают, если IE перехваченный, могут не работать. Другой вариант, просто из атрибута src получать URL имиджа и загружать его функцией UrlDownloadToFile - будет работать 100%.
|
|
|
Записан
|
|
|
|
Kultura
Помогающий
Offline
|
|
« Ответ #22 : 29-11-2008 17:46 » |
|
С IViewObject::Draw дело не пошло: не совсем понятно, что там должно быть на месте 1-го и 2-го HDC (hicTargetDev, hdcDraw). Вот в таком виде, вызов Draw (...) ругается E_HANDLE: HDC hImageDC = CreateEnhMetaFile (NULL, NULL, NULL, NULL); HDC hScreenDC = GetDC(0); RECTL rcSource = {0, 0, iScrollWidth, iScrollHeight}; hr = pViewObject->Draw (DVASPECT_CONTENT, 1, NULL, NULL, hScreenDC, hImageDC, & rcSource, NULL, NULL, 0);
UrlDownloadToFile сделал, работает Теперь не получается освоиться с классом Image, который вы мне рекомендовали ранее. Не получается его подключить. Какие #include ... нужно делать? В документации нашел "Declared in Gdiplusheaders.h, include gdiplus.h", "gdiplus.lib", что-то не помогает. В интернете еще нашел что-то про namespace System.xxx Подскажите
|
|
|
Записан
|
|
|
|
Kultura
Помогающий
Offline
|
|
« Ответ #23 : 02-12-2008 13:32 » |
|
Все, разобрался с Image. Приступаю к дешифровке кода.
Каптча состоит из букв и цифр на цветном фоне, искаженных алгоритмом MultiWave. Никто ранее не сталкивался? Вообще, часто такие каптчи встречаются, но дешифратора в нете не нашел.
|
|
|
Записан
|
|
|
|
|