Blog. Just Blog

Правильное принудительное обновление иконок в трее

Версия для печати Добавить в Избранное Отправить на E-Mail | Категория: Образ мышления: Assembler | Автор: ManHunter
Несколько лет назад я опубликовал в блоге статью о принудительном обновлении иконок в трее, в которой опрометчиво заявил, что это единственный способ решения задачи. Все течет, все изменяется, поэтому представляю вашему вниманию новую статью-опровержение, в которой расскажу, как правильно обновлять иконки в трее.

Суть задачи остается прежней: некое приложение аварийно завершило работу, в трее остался его значок. Нужно из своего приложения обновить системный трей таким образом, чтобы этот значок убрать. Все трудности этой задачи я подробно описал в статье по ссылке выше, поэтому повторяться не буду и перейду сразу к решению. За основу взят код получения списка иконок в трее. При переборе проверяется наличие окна, которому должна принадлежать иконка. Если окна нет, значит и иконки существовать не должно. Такие иконки удаляются при помощи функции Shell_NotifyIcon с параметром NIM_DELETE.
  1. ; Сегмент данных
  2. section '.data' data readable writeable
  3.  
  4. ; Структура пользовательских данных иконки
  5. struct EXTRADATA
  6.         Wnd dd ?
  7.         uID dd ?
  8. ends
  9.  
  10. class1     db 'Shell_TrayWnd',0    ; Название класса окна трея
  11. class2     db 'TrayNotifyWnd',0    ; Название класса панели уведомлений
  12. class3     db 'SysPager',0         ; Трей
  13. class4     db 'ToolbarWindow32',0  ; Панель с иконками
  14.  
  15. ; Структура для кнопки
  16. button     TBBUTTON
  17. ; Структура для пользовательских данных иконки
  18. extra      EXTRADATA
  19. ; Структура для работы с иконкой в трее
  20. nid        NOTIFYICONDATA
  21.  
  22. hToolbar   dd ?    ; Хэндл окна с иконками
  23. IconsCount dd ?    ; Количество иконок в трее
  24. ProcId     dd ?    ; Id процесса
  25. hProcess   dd ?    ; Хэндл процесса
  26. lpData     dd ?    ; Указатель на блок памяти
  27. BytesRead  dd ?    ; Количество прочитанных символов
  1.         ; Найти окно трея
  2.         invoke  FindWindow,class1,NULL
  3.         or      eax,eax
  4.         jz      exit_process
  5.  
  6.         ; Найти панель уведомлений
  7.         invoke  FindWindowEx,eax,NULL,class2,NULL
  8.         or      eax,eax
  9.         jz      exit_process
  10.  
  11.         ; Найти трей
  12.         invoke  FindWindowEx,eax,NULL,class3,NULL
  13.         or      eax,eax
  14.         jz      exit_process
  15.  
  16.         ; Найти панель иконок в трее
  17.         invoke  FindWindowEx,eax,NULL,class4,NULL
  18.         or      eax,eax
  19.         jz      exit_process
  20.  
  21.         ; Сохранить хэндл окна с иконками
  22.         mov     [hToolbar],eax
  23.  
  24.         ; Получить количество иконок в трее
  25.         invoke  SendMessage,eax,TB_BUTTONCOUNT,0,0
  26.         or      eax,eax
  27.         jz      exit_process
  28.  
  29.         ; Сохранить количество иконок в трее
  30.         mov     [IconsCount],eax
  31.  
  32.         ; Получить ID процесса-владельца трея
  33.         invoke  GetWindowThreadProcessId,[hToolbar],ProcId
  34.         ; Открыть процесс с для чтения и записи
  35.         invoke  OpenProcess,PROCESS_VM_OPERATION or PROCESS_VM_READ,\
  36.                 FALSE,[ProcId]
  37.         or      eax,eax
  38.         ; Фокус не удался
  39.         jz      exit_process
  40.  
  41.         ; Сохранить хэндл процесса-владельца трея
  42.         mov     [hProcess],eax
  43.  
  44.         ; Выделить блок памяти в контексте процесса
  45.         invoke  VirtualAllocEx,[hProcess],NULL,dword sizeof.TBBUTTON,\
  46.                 MEM_COMMIT,PAGE_READWRITE
  47.         or      eax,eax
  48.         jz      exit_process
  49.  
  50.         ; Сохранить указатель на блок памяти
  51.         mov     [lpData],eax
  52.  
  53.         ; Перебрать все иконки в трее
  54. loc_loop:
  55.         ; Все иконки обработали?
  56.         cmp     [IconsCount],0
  57.         je      clear_memory
  58.  
  59.         ; Следующая иконка
  60.         dec     [IconsCount]
  61.  
  62.         ; Получить иконку из трея с индексом IconsCount
  63.         invoke  SendMessage,[hToolbar],TB_GETBUTTON,[IconsCount],[lpData]
  64.         ; Прочитать структуру иконки
  65.         invoke  ReadProcessMemory,[hProcess],[lpData],button,\
  66.                 dword sizeof.TBBUTTON,BytesRead
  67.         or      eax,eax
  68.         jz      clear_memory
  69.         ; Прочиталась вся структура?
  70.         cmp     [BytesRead],sizeof.TBBUTTON
  71.         jnz     clear_memory
  72.  
  73.         ; Прочитать пользовательские данные иконки
  74.         invoke  ReadProcessMemory,[hProcess],[button.dwData],extra,\
  75.                 dword sizeof.EXTRADATA,BytesRead
  76.         or      eax,eax
  77.         jz      clear_memory
  78.         ; Прочиталась вся структура?
  79.         cmp     [BytesRead],sizeof.EXTRADATA
  80.         jnz     clear_memory
  81.  
  82.         ; Это скрытая иконка?
  83.         mov     eax,[extra.uID]
  84.         and     eax,80000000h
  85.         or      eax,eax
  86.         ; Да, пропустить
  87.         jnz     loc_loop
  88.  
  89.         ; Окно процесса существует?
  90.         invoke  IsWindow,[extra.Wnd]
  91.         or      eax,eax
  92.         jnz     loc_loop
  93.  
  94.         ; Удалить иконку, у которой нет родителя
  95.         mov     [nid.cbSize],sizeof.NOTIFYICONDATA
  96.         mov     eax,[extra.Wnd]
  97.         mov     [nid.hWnd],eax
  98.         mov     eax,[extra.uID]
  99.         mov     [nid.uID],eax
  100.         invoke  Shell_NotifyIcon,NIM_DELETE,nid
  101.         jmp     loc_loop
  102.  
  103. clear_memory:
  104.         ; Очистить память и ресурсы
  105.         invoke  VirtualFreeEx,[hProcess],[lpData],0,MEM_RELEASE
  106.         invoke  CloseHandle,[hProcess]
  107.  
  108. exit_process:
Способ простой, надежный и не вызывает побочных эффектов типа реакции приложений при эмуляции движения курсора над треем.

Есть еще один способ удаления иконки без использования функции Shell_NotifyIcon. Основной код останется практически таким же. Изменения коснутся только следующих участков:
  1. hTaskbar   dd ?    ; Хэндл окна трея
  2.         ...
  3.         ...
  4.         ; Найти окно трея
  5.         invoke  FindWindow,class1,NULL
  6.         or      eax,eax
  7.         jz      exit_process
  8.         mov     [hTaskbar],eax
  9.         ...
  10.         ...
  11.         ; Окно процесса существует?
  12.         invoke  IsWindow,[extra.Wnd]
  13.         or      eax,eax
  14.         jnz     loc_loop
  15.  
  16.         ; Удалить иконку, у которой нет родителя
  17.         invoke  SendMessage,[hToolbar],TB_DELETEBUTTON,[IconsCount],0
  18.         invoke  SendMessage,[hTaskbar],WM_WININICHANGE,0,0
  19.         jmp     loc_loop
Также имейте в виду, что из 32-битного приложения просто получить доступ к памяти 64-битного Проводника не получится. В статье код приведен именно для 32-битного приложения, чтобы его расширить на обе разрядности системы, надо воспользоваться функциями чтения памяти, описанными в этой статье. Это невероятно сильное колдунство, поэтому приведу код целиком, но сперва прокомментирую ситуацию. Во-первых, нельзя просто взять и из 32-битного приложения зарезервировать память в контексте 64-битного процесса. Функция VirtualAllocEx, вызываемая из 32-битного процесса, вернет указатель размера DWORD, в то время как адресация памяти целевого процесса имеет разрядность QWORD и, соответственно, может выходить за пределы памяти, доступной для нашей программы. Чтобы решить эту проблему, надо последовательно обойти карту памяти процесса Проводника и найти в ней свободный блок, который находится в доступном нам адресном пространстве. Таким образом мы можем выделить память из 32-битного приложения в контексте 64-битного. Во-вторых, структура TBBUTTON также меняется с учетом 64-битной системы. Ну и напоследок, доступ к структуре EXTRADATA все равно придется получать через NtWow64ReadVirtualMemory64.
  1. struct  MEMORY_BASIC_INFORMATION
  2.         BaseAddress          dd ?
  3.         AllocationBase       dd ?
  4.         AllocationProtect    dd ?
  5.         RegionSize           dd ?
  6.         State                dd ?
  7.         Protect              dd ?
  8.         Type                 dd ?
  9. ends
  10.  
  11. struct EXTRADATA
  12.         Wnd dd ?
  13.         uID dd ?
  14. ends
  15.  
  16. struct TBBUTTON64
  17.         iBitmap   dd ?
  18.         idCommand dd ?
  19.         fsState   db ?
  20.         fsStyle   db ?
  21.         Reserved  rb 6
  22.         dwData    dq ?
  23.         iString   dq ?
  24. ends
  25.  
  26. class1     db 'Shell_TrayWnd',0    ; Название класса окна трея
  27. class2     db 'TrayNotifyWnd',0    ; Название класса панели уведомлений
  28. class3     db 'SysPager',0         ; Трей
  29. class4     db 'ToolbarWindow32',0  ; Панель с иконками
  30.  
  31. ntdll      db 'ntdll.dll',0
  32. nwmname    db 'NtWow64ReadVirtualMemory64',0
  33.  
  34. ; Структура для кнопки
  35. button     TBBUTTON64
  36. ; Структура для пользовательских данных иконки
  37. extra      EXTRADATA
  38. ; Структура для чтения памяти процесса
  39. mbi        MEMORY_BASIC_INFORMATION
  40.  
  41. hToolbar   dd ?    ; Хэндл окна с иконками
  42. hTaskbar   dd ?    ; Хэндл окна трея
  43. IconsCount dd ?    ; Количество иконок в трее
  44. ProcId     dd ?    ; Id процесса
  45. hProcess   dd ?    ; Хэндл процесса
  46. lpData     dd ?    ; Указатель на блок памяти
  47. BytesRead  dd ?    ; Количество прочитанных символов
  48. dRead      dd ?    ; Адрес NtWow64ReadVirtualMemory64
  1.         ; Найти окно трея
  2.         invoke  FindWindow,class1,NULL
  3.         or      eax,eax
  4.         jz      exit_process
  5.  
  6.         mov     [hTaskbar],eax
  7.  
  8.         ; Найти панель уведомлений
  9.         invoke  FindWindowEx,eax,NULL,class2,NULL
  10.         or      eax,eax
  11.         jz      exit_process
  12.  
  13.         ; Найти трей
  14.         invoke  FindWindowEx,eax,NULL,class3,NULL
  15.         or      eax,eax
  16.         jz      exit_process
  17.  
  18.         ; Найти панель иконок в трее
  19.         invoke  FindWindowEx,eax,NULL,class4,NULL
  20.         or      eax,eax
  21.         jz      exit_process
  22.  
  23.         ; Сохранить хэндл окна с иконками
  24.         mov     [hToolbar],eax
  25.  
  26.         ; Получить количество иконок в трее
  27.         invoke  SendMessage,eax,TB_BUTTONCOUNT,0,0
  28.         or      eax,eax
  29.         jz      exit_process
  30.  
  31.         ; Сохранить количество иконок в трее
  32.         mov     [IconsCount],eax
  33.  
  34.         ; Получить ID процесса-владельца трея
  35.         invoke  GetWindowThreadProcessId,[hToolbar],ProcId
  36.         ; Открыть процесс для чтения и записи
  37.         invoke  OpenProcess,PROCESS_ALL_ACCESS,FALSE,[ProcId]
  38.         or      eax,eax
  39.         ; Фокус не удался
  40.         jz      exit_process
  41.  
  42.         ; Сохранить хэндл процесса-владельца трея
  43.         mov     [hProcess],eax
  44.  
  45.         ; NtWow64ReadVirtualMemory64
  46.         invoke  GetModuleHandle,ntdll
  47.         invoke  GetProcAddress,eax,nwmname
  48.         or      eax,eax
  49.         jz      exit_process
  50.  
  51.         mov     [dRead],eax
  52.  
  53.         ; Выделить блок памяти в контексте процесса
  54.         xor     ebx,ebx
  55. loc_allocate_memory:
  56.         invoke  VirtualQueryEx,[hProcess],ebx,mbi,\
  57.                 sizeof.MEMORY_BASIC_INFORMATION
  58.         or      eax,eax
  59.         jz      error_memory
  60.         cmp     [mbi.State],MEM_FREE
  61.         jne     @f
  62.         invoke  VirtualAllocEx,[hProcess],dword [mbi.BaseAddress],\
  63.                 dword sizeof.TBBUTTON64,MEM_COMMIT+MEM_RESERVE,PAGE_READWRITE
  64.         or      eax,eax
  65.         jnz     memory_ok
  66. @@:
  67.         add     ebx,[mbi.RegionSize]
  68.         cmp     ebx,0x7FFFFFFF
  69.         jb      loc_allocate_memory
  70. error_memory:
  71.         jmp     exit_process
  72.  
  73. memory_ok:
  74.         ; Сохранить указатель на блок памяти
  75.         mov     [lpData],eax
  76.  
  77.         ; Перебрать все иконки в трее
  78. loc_loop:
  79.         ; Все иконки обработали?
  80.         cmp     [IconsCount],0
  81.         je      clear_memory
  82.  
  83.         ; Следующая иконка
  84.         dec     [IconsCount]
  85.  
  86.         ; Получить иконку из трея с индексом IconsCount
  87.         invoke  SendMessage,[hToolbar],TB_GETBUTTON,[IconsCount],[lpData]
  88.  
  89.         ; Прочитать структуру иконки
  90.         invoke  ReadProcessMemory,[hProcess],[lpData],button,\
  91.                 dword sizeof.TBBUTTON64,BytesRead
  92.         or      eax,eax
  93.         jz      clear_memory
  94.         ; Прочиталась вся структура?
  95.         cmp     [BytesRead],sizeof.TBBUTTON64
  96.         jne     clear_memory
  97.  
  98.         ; Прочитать пользовательские данные иконки
  99.         mov     esi,[dRead]
  100.         push    0
  101.         push    0
  102.         push    sizeof.EXTRADATA
  103.         push    extra
  104.         push    dword [button.dwData+4]
  105.         push    dword [button.dwData]
  106.         stdcall esi,[hProcess]
  107.         or      eax,eax
  108.         jnz     clear_memory
  109.  
  110.         ; Это скрытая иконка?
  111.         mov     eax,[extra.uID]
  112.         and     eax,80000000h
  113.         or      eax,eax
  114.         ; Да, пропустить
  115.         jnz     loc_loop
  116.  
  117.         ; Окно процесса существует?
  118.         invoke  IsWindow,dword [extra.Wnd]
  119.         or      eax,eax
  120.         jnz     loc_loop
  121.  
  122.         ; Удалить иконку, у которой нет родителя
  123.         invoke  SendMessage,[hToolbar],TB_DELETEBUTTON,[IconsCount],0
  124.         invoke  SendMessage,[hTaskbar],WM_WININICHANGE,0,0
  125.         jmp     loc_loop
  126.  
  127. clear_memory:
  128.         ; Очистить память и ресурсы
  129.         invoke  VirtualFreeEx,[hProcess],[lpData],0,MEM_RELEASE
  130.         invoke  CloseHandle,[hProcess]
  131.  
  132. exit_process:
Оффтоп: точно такой же способ выделения памяти из 32-битного приложения в 64-битном можно использовать не только для перебора иконок, но также для инжекта и создания удаленного потока через CreateRemoteThread. Так что очень рекомендую запомнить этот трюк, возможно, в будущем он вам может пригодиться.

На своих компьютерах я иногда наблюдаю ситуацию, когда приложение вроде бы создало свою иконку в трее, но при этом иконка не отображается. Пока мне не удалось выяснить, с чем это связано, но решение я нашел. Если приложение написано по всем правилам, то оно должно обрабатывать ситуацию, когда произошел сбой системы и требуется обновить иконку в трее. Для этого надо отправить широковещательное сообщение TaskbarCreated.
  1. szTBC      db 'TaskbarCreated',0
  2.         ...
  3.         ...
  4.         ; Отправить всем окнам сообщение о необходимости
  5.         ; обновить свои иконки в трее (кто поддерживает)
  6.         HWND_BROADCAST = 0xFFFF
  7.         invoke  RegisterWindowMessage,szTBC
  8.         invoke  SendMessage,HWND_BROADCAST,eax,NULL,NULL
К сожалению, при использовании этого способа трей может "дергаться", так как приложения удаляют свои иконки перед добавлением их в трей обратно. Дело тут не в реализации способа, просто приложения так запрограммированы. Можете использовать трюк с TaskbarCreated совместно с удалением неиспользуемых иконок, можете не использовать, это уже на ваше усмотрение. Но знать о нем будет не лишним.

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

Пример программы с исходным текстом (FASM)Пример программы с исходным текстом (FASM)

True.Force.Update.Tray.Demo.zip (12,624 bytes)


Поделиться ссылкой ВКонтакте
Просмотров: 2775 | Комментариев: 9

Внимание! Статья опубликована больше года назад, информация могла устареть!

Комментарии

Отзывы посетителей сайта о статье
strah (17.12.2022 в 19:13):
Ivan_Alone, спасибо за код, пригодилось!
Только поменял
EXTRADATA extra;
на
struct EXTRADATA {
  HWND Wnd;
  UINT uID;
} extra{};
а то описание структуры где-то потерялось.
Кстати, вот и кресты, а не чистый си :)
Kyklish (04.08.2021 в 10:58):
Аварийное завершение MPC-HC оставляет иконки встроенных LAV декодеров&сплиттера в трее.

Из моих наблюдений:
Shell_NotifyIcon с параметром NIM_DELETE - заметчательно работает.
SendMessage с параметром TB_DELETEBUTTON - убивает иконки конкретно LAV декодеров&сплиттера (при повторном запуске MPC-HC появляются пустые иконки без меню по правому щелчку мыши) до следущего входа в систему.
ManHunter (31.01.2021 в 00:34):
А что тут крестового? Обычный чистый Си, обычные WinAPI.
Ivan_Alone (30.01.2021 в 22:28):
Если кому-то вдруг нужно, вот реализация того же на C++. Обновил так сказать свой предыдущий комментарий в предыдущей публикации по этой теме.

Автору - спасибо за решение, очень полезно!
А про прошлый комментарий - ну не знал я про новую статью, она не выдаётся в поиске раньше старой, да и ссылок не было =)

HWND finder = FindWindow(L"Shell_TrayWnd", NULL);

if (finder) {
finder = FindWindowEx(finder, NULL, L"TrayNotifyWnd", NULL);

if (finder) {
finder = FindWindowEx(finder, NULL, L"SysPager", NULL);

if (finder) {
finder = FindWindowEx(finder, NULL, L"ToolbarWindow32", NULL);

int IconsCount = SendMessage(finder, TB_BUTTONCOUNT, 0, 0);

if (IconsCount) {
DWORD ProcId;
GetWindowThreadProcessId(finder, &ProcId);

HANDLE hProcess = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_READ, FALSE, ProcId);

if (hProcess) {
LPVOID lpData = VirtualAllocEx(hProcess, NULL, sizeof(TBBUTTON), MEM_COMMIT, PAGE_READWRITE);

if (lpData) {
for (;;) {
if (IconsCount <= 0) {
break;
}
IconsCount--;

TBBUTTON button;
SIZE_T BytesRead;

SendMessage(finder, TB_GETBUTTON, IconsCount, (LPARAM)lpData);
if (ReadProcessMemory(hProcess, lpData, &button, sizeof(TBBUTTON), &BytesRead)) {
if (BytesRead != sizeof(TBBUTTON)) {
break;
}
EXTRADATA extra;
if (ReadProcessMemory(hProcess, (LPVOID)button.dwData, &extra, sizeof(EXTRADATA), &BytesRead)) {
if (BytesRead != sizeof(EXTRADATA)) {
break;
}

if (extra.uID & 0x80000000) {
continue;
}

if (IsWindow(extra.Wnd)) {
continue;
}

NOTIFYICONDATA nid;
nid.cbSize = sizeof(NOTIFYICONDATA);
nid.hWnd = extra.Wnd;
nid.uID = extra.uID;

// Способ 1
Shell_NotifyIcon(NIM_DELETE, &nid);

// Способ 2
/*SendMessage(finder, TB_DELETEBUTTON, IconsCount, 0);
SendMessage(finder, WM_WININICHANGE, 0, 0);*/
}
} else {
break;
}
}
VirtualFreeEx(hProcess, lpData, 0, MEM_RELEASE);
CloseHandle(hProcess);
}
}
}
}
}
}
Кулибин (14.07.2017 в 03:07):
Большое спасибо!! :3
ManHunter (05.07.2017 в 18:51):
Что мешает просто попробовать все варианты и выбрать нужный?

ЦитатаПримеры не глядел

"Пастернака не читал, но осуждаю" :)
Doxtur (05.07.2017 в 18:23):
По работе использую OPC шлюз, после потери связи не факт что он подключится, хотя будет писать что все ок. Приходилось перезагружать периодически, за пару месяцев оставалось просто гигантское количество иконок.

Я сделал немного по другому - сделал две версии одна для настроек с иконкой в трее, а во второй версии грубым битхаком вынес процедуру создания иконки.

Примеры не глядел - можно ли сделать консольку с параметрами - типа /1 первый способ, /2 - второй способ и т.п. ? Выберу годный и буду юзать и Вас вспоминать добрым словом)
ManHunter (30.06.2017 в 00:45):
После бессонной ночи в отладчике добавил вариант обновления иконок в трее из 32-битного приложения, запущенного на 64-битной системе. Очень рекомендую внимательно почитать. Архив обновлен.
ManHunter (27.06.2017 в 14:05):
Добавил еще один вариант удаления мертвых иконок. Архив обновил.

Добавить комментарий

Заполните форму для добавления комментария
Имя*:
Текст комментария (не более 2000 символов)*:

*Все поля обязательны для заполнения.
Комментарии, содержащие рекламу, ненормативную лексику, оскорбления и т.п., а также флуд и сообщения не по теме, будут удаляться. Нарушителям может быть заблокирован доступ к сайту.
Наверх
Powered by PCL's Speckled Band Engine 0.2 RC3
© ManHunter / PCL, 2008-2024
При использовании материалов ссылка на сайт обязательна
Время генерации: 0.08 сек. / MySQL: 2 (0.0059 сек.) / Память: 4.5 Mb
Наверх