Защо съобщението TVM_GETITEM се проваля в дървовидни изгледи comctl32.ocx или mscomctl.ocx?

Написах функция, която може да даде текста на елемент от дървовиден изглед, дори ако дървовидният изглед е в отдалечен процес. Функцията разпределя две части памет в отдалечения процес, попълва структура TVITEM (която е копирана в отдалечения процес), изпраща съобщение TVM_GETITEM и накрая чете съдържанието на второто отдалечено парче памет обратно в локален буфер. Това е кодът:

std::string getTreeViewItemText( HWND treeView, HTREEITEM item )
{
    DWORD pid;
    ::GetWindowThreadProcessId( treeView, &pid );

    HANDLE proc = ::OpenProcess( PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE, FALSE, pid );
    if ( !proc )
        // handle error

    TVITEM tvi;
    ZeroMemory( &tvi, sizeof(tvi) );

    LPVOID tvi_ = ::VirtualAllocEx( proc, NULL, sizeof(tvi), MEM_COMMIT, PAGE_READWRITE);
    if ( !tvi_ )
        // handle error

    TCHAR buffer[100] = { 'X' };

    LPVOID txt_ = ::VirtualAllocEx( proc, NULL, sizeof(buffer), MEM_COMMIT, PAGE_READWRITE );
    if ( !txt_ )
        // handle error

    tvi.mask = TVIF_TEXT | TVIF_HANDLE;
    tvi.pszText =  (LPTSTR)txt_;
    tvi.cchTextMax = sizeof(buffer) / sizeof(buffer[0] );
    tvi.hItem = item;

    if ( !::WriteProcessMemory( proc, tvi_, &tvi, sizeof(tvi), NULL ) )
        // handle error

    if ( !::SendMessage( treeView, TVM_GETITEM, 0, (LPARAM)tvi_ ) )
        // handle error

    if ( !::ReadProcessMemory( proc, (LPCVOID)txt_, buffer, sizeof( buffer ), NULL ) )
        // handle error

    ::VirtualFreeEx( proc, tvi_, 0, MEM_RELEASE );

    ::VirtualFreeEx( proc, txt_, 0, MEM_RELEASE );

    ::CloseHandle( proc );

    return buffer;
}

Този код работи много добре с обикновените дървовидни изгледи, които получавате, когато предавате името на класа WC_TREEVIEW на CreateWindow. Забелязах обаче, че не работи с по-новите дървета, предоставени от MS Common Controls v5 (comctl32.ocx) или MS Common Controls v6 (mscomctl.ocx). В тези случаи върнатият текст винаги е празен (буферът е само нули). Също така забелязах, че извикването SendMessage връща нула (следователно се включва обработката на грешки, обозначена с // handle error коментарите по-горе). Не ми е ясно дали това наистина означава грешка, във всеки случай буферът е пълен с всички нули.

Всички други съобщения за дървовиден изглед (като TVM_GETITEMRECT) изглежда работят перфектно.

Някой знае ли защо е така? Опитах се да си поиграя с флага UNICODE (забелязах, че TVM_GETITEM е дефиниран като TVM_GETITEMA или TVM_GETITEMW), но това не изглежда да помогне.


person Frerich Raabe    schedule 11.02.2010    source източник
comment
След като SendMessage върне 0, опитайте GetLastError. Какво връща? MSDN: Microsoft Windows Vista и по-нова версия. Когато съобщение е блокирано от UIPI, последната грешка, извлечена с GetLastError, е зададена на 5 (достъпът е отказан).   -  person Igor    schedule 17.02.2010
comment
@Игор: Добра идея, но без успех: извикването на GetLastError() след SendMessage() дава код на грешка 0. Може би кодът на грешката е зададен в отдалечения процес, защото е възникнала някаква грешка при предаването на TVM_GETITEM?   -  person Frerich Raabe    schedule 17.02.2010


Отговори (3)


Добре, нека да опитаме още веднъж.

По-новите TreeViews очакват TVITEMEX вместо TVITEM и тъй като няма обичайно поле cbSize, контролата не може да каже коя версия получава и приема TVITEMEX. Може би има проблем с това, че дървовидният изглед не може да има достъп до последните членове на TVITEMEX, защото не е разпределена памет. Опитайте да използвате TVITEMEX или да разпределите малко повече памет за TVITEM, отколкото действително се изисква.

Също така помислете за това

Върнатият текст не е задължително да се съхранява в оригиналния буфер, подаден от приложението. Възможно е pszText да сочи към текст в нов буфер, вместо да го постави в стария буфер.

Следователно може да се наложи да четете от друга част от паметта на процеса.

И буферът се нулира, защото VirtualAllocEx нулира паметта.

И като последно и вероятно безполезно средство, опитайте да използвате MEM_RESERVE|MEM_COMMIT вместо само MEM_COMMIT.

person GSerg    schedule 18.02.2010
comment
Благодаря за вашите предложения! Пробвах с TVITEMEX, но ефект нямаше. В крайна сметка се оказа, че трябва изрично да проверя дали отдалечената страна е Unicode прозорец или не, защото двупосочната транслация на SendMessage по подразбиране между ANSI и Unicode процеси не се прилага. Вижте моя отговор за подробности. - person Frerich Raabe; 18.02.2010

Кодът не работи според очакванията, ако е компилиран с дефиниран UNICODE, но отдалеченият процес не работи (или обратното). Трябва първо да извикате IsWindowUnicode на манипулатора treeView, за да проверете дали отдалечената страна очаква Unicode съобщения.

Това е необходимо, тъй като стандартното двупосочно маршалиране, което SendMessage прави, не е достатъчно в този случай: трябва да изпратите две напълно различни прозоречни съобщения в зависимост от това дали отдалечената страна е Unicode прозорец или не. Ако е Unicode, използвайте SendMessageW с TVM_GETITEMW. Ако е ANSI, използвайте SendMessageA с TVM_GETITEMA.

Това се отнася за всички общи контроли, но не и за основния набор от контроли (който използва съобщения в прозореца ‹ 1024).

Също така вярвам, че кодът ще се счупи, ако бъде компилиран в 64-битов двоичен файл, но отдалеченият процес е 32-битов (или обратното). Това е така, защото кодът копира своя локален (да речем: 64-битов) TVITEM в отдалечения процес и след това очаква отдалеченият процес да го прочете, както се очаква, докато работи със съобщението TVM_GETITEM(A|W). Размерът на структурата обаче може да е различен (поради различни размери на показалеца).

person Frerich Raabe    schedule 18.02.2010
comment
Странно, всичко работи добре тук, когато изпращам TVM_GETITEMA към Windows 7 дървовидни изгледи, които са unicode (но обратното не е добре, това е вярно). Проблемът с указателя обаче е много добър момент. - person GSerg; 18.02.2010

Използвайте Spy++, за да видите дали дървовидният изглед обработва WM_NOTIFY съобщения с NM_CUSTOMDRAW флаг за уведомяване. Ако стане, значи лош късмет. Действителните данни се съхраняват вътрешно по някакъв начин и имате малък шанс да ги извадите.

Това важи еднакво и за предишните версии на Windows BTW.

person GSerg    schedule 11.02.2010
comment
Добър съвет! Изглежда обаче, че Spy++, който идва с Visual Studio 2008, не показва никакви съобщения в прозореца за бележник. Работи добре с напр. Firefox, но за notepad не виждам нищо. Проверих два пъти дали регистрирането наистина е активирано и всички типове съобщения са активирани. Жалко :-/ - person Frerich Raabe; 11.02.2010
comment
След това проверете и настройките на UAC. Spy++ трябва да може да работи с повишени права. Току-що проверено, всички съобщения в Notepad са там. - person GSerg; 11.02.2010
comment
За съжаление, изглежда не е включен WM_NOTIFY. - person Frerich Raabe; 17.02.2010