Własny zasobnik systemowy (tray)

crayze

Witam wszystkich. Jest to mój pierwszy artykuł, więc proszę o wyrozumiałość :>

Artykuł skierowany jest przede wszystkim do koderów powłok na winde. Swego czasu sam tworzyłem tego typu program, aby zastąpić nim jakże powolny explorer.exe :>

Rzecz tyczy się przejęcia władzy nad zasobnikiem systemowym(potocznie zwanym tray’em). Każda szanująca się powłoka musi go w mniejszym czy większym stopniu obsługiwać. Informacji na ten temat w Internecie jest mało, nie mówiąc już o polskich stronach :>

Oczywiście przy czytaniu tego arta wymagam znajomości komunikacji z zasobnikiem, czyli żeby wiedzieć jak wepchnąć swoja ikonę do tray’a i takie tam, czyli ogólnie znajomość struktury NOTIFYICONDATA i funkcji Shell_NotifyIcon(); tyle wystarczy do komunikacji z tray’em. Nauka umieszczania i obsługi ikonki w tray’u nie jest celem tego artykułu. Do tego odsyłam do MSDN, czy na polskiej stronie „Dark Cult of C++” jest całkiem dobrze opisany proces pracy z taką ikonką.

DO RZECZY

W praktyce stosuje się dwie metody przechwycenia komunikatów tray’a, bo właśnie na przejęciu komunikatów wędrujących do tray’a nam zależy, gdyż funkcja Shell_NotifyIcon() nie robi nic innego jak wysyła odpowiedni komunikat do zasobnika w windzie.

Pierwszy sposób powinien być stosowany przy pracy razem z explorer’em, gdyż nie „zakłóca on pracy” tray’a explorer’owego. Polega on na założeniu globalnego HOOK’a(HHOOK) w systemie i przechwytywaniu komunikatów wędrujących do tray’a, jeżeli w hooku nie będziemy modyfikować komunikatów, to będą one nadal trafiać do traya explorer’a, a my będziemy je tylko „podglądać” :> Sposób ten wymaga trochę pracy, bo przecież musimy napisać oddzielną DLL’kę z hookiem, no i musimy mieć już jakieś doświadczenie z WindowsAPI, aby zakładać globalne HOOKi.

Drugi sposób, który przedstawię w tym arcie, będzie prostszy i wymaga tylko podstawowej znajomości WinAPI. Można się domyślać, że skoro tamten nie koliduje z explorerem to ten tak :> Sposób ten polega na „podszyciu się” za pasek explorera. W ten sposób komunikaty będą już trafiać tylko do nas, a jeżeli pasek Explorera będzie nadal uruchomiony, tray explorera po prostu „zamarznie”, nie będą już do niego dochodzić komunikaty :>

ZACZYNAMY – PRZYGOTOWANIE OKNA

Skoro mamy się podszywać za Explorer, musimy sobie stworzyć wirtualne(albo i nie) okno o takich samych nazwach jakie ma Explorer.

W klasie okna WNDCLASSEX jako pole lpszClassName musimy podać ciąg: „Shell_TrayWnd”, bo rzecz jasna tak nazywa się klasa okna Explorera. Następnie tworzymy okno(HWND). Jako nazwę klasy w drugim parametrze CreateWindowEx musimy podać także „Shell_TrayWnd”. Okno także musi mieć rozszerzony styl WS_EX_TOPMOST(pierwszy parametr CreateWindowEx). Oczywiście okno to może być czysto wirtualne, czyli mieć parametry wysokości i szerokości po 0 (nie widać go), ważne aby takie okno istniało programowo, aby mogły do niego docierać komunikaty.

KOMUNIKAT GOTOWOŚCI

Mając okno podszywające się pod okno Explorera, musimy wysłać do wszystkich okien w systemie wiadomość o tym, że tray stał się na nowo aktywny. Komunikat taki musimy zarejestrować funkcją RegisterWindowMessage(), jako parametr podajemy: „TaskbarCreated”. Aby wysłać ten komunikat do wszystkich okien możemy użyć funkcji EnumWindows(). Komunikat ten trafia do każdego okna. Ten komunikat zawsze trafi w każdym oknie do DefWindowProc() i Windows sam w obsłudze tego komunikatu doda ikonę do naszego zasobnika, jeżeli tylko program wcześniej ją miał w trayu.

OBSŁUGA KOMUNIKATU

W ten sposób stworzyliśmy już okno, do którego zaczną napływać komunikaty traya. W procedurze zdarzeniowej okna traya, powinniśmy obsłużyć komunikat WM_COPYDATA.
W tym komunikacie interesuje nas tylko wartość LPARAM, gdyż jest ona wskaźnikiem do struktury COPYDATASTRUCT. Na początek warto sprawdzić czy pole dwData jest równe 1. Następnie możemy zająć się polem lpData, gdyż to w nim mamy najważniejsze informacje. Pierwsze 4 bajty to….ekmm nie pamiętam co to jest :>, pewnie identyfikator że jest to komunikat traya. Następne 4 bajty to polecenie traya, czyli pierwszy parametr, który został podany w Shell_NotifyIcon(np. NIM_ADD, NIM_MODIFY, NIM_DELETE). I kolejne bajty to wskaźnik na strukturę NOTIFYICONDATA(tu nie jestem pewien co do tego ile bajtów zajmuje wskaźnik; w architekturze 32bit, na pewno 4, ale co gdy nasz system działa jako 64bit, czy wskaźnik w tym miejscu będzie zajmował 8 bajtów?, tego nie wiem), która została wysłana z tym komunikatem. W niej mamy najważniejsze informacje, dzięki którym możemy obsługiwać traya. Właściwie to by było na tyle. Sprawiliśmy, że komunikaty trafiają do naszego programu, oraz wiemy jak je odczytywać :> Teraz już pozostaje kwestia napisania własnego traya, bo rysowanie ikon i wysyłanie komunikatów do właściciela ikonki, po naciśnięciu na daną ikonę, leży już w geście naszego programu, czy wreszcie wyświetlanie tekstów po najechaniu na ikonkę czy wyświetlanie tzw. podpowiedzi baloników (WinXP), to wszystko musi realizować nasz program, jako tray, my dostajemy do ręki tylko komunikat wysłany z jakiejś aplikacji.

tray.png

UWAGA: niektóre treści mogą nie być pełne lub niedokładnie, sposoby te nie są oficjalnie opublikowane przez M$, ale odkryte są metodami „prób i błędów”, dlatego mogą być jakieś drobne pomyłki, niedokładności.

A to kod przykładowego okienka traya, wraz z obsługa ikon i komunikatów zwrotnych:

#include <vector>
#include <windows.h>

using std::vector;

struct STrayIcon  //struktura pojedynczej ikony w trayu
{
  HWND hwnd;  //okno będące właścicielem ikony
  UINT uID;  //numer identyfikacyjny ikony
  HICON hIcon;  //uchwyt do ikony
  UINT uMessage;  //numer zwracanego komunikatu w razie naciśnięcia na ikonę
};

HWND hWnd;
vector<STrayIcon*> TrayIcons;  //vector z istniejacymi ikonami

LRESULT CALLBACK WindowProcedure(HWND,UINT,WPARAM,LPARAM);
bool CreateTray(int);  //tworzy okno traya
void SendStartupTray();  //wysyła powiadomienie do wszystkich okien
void TrayMessage(COPYDATASTRUCT*);  //obsługa komunikatu traya

int FindTrayIcon(NOTIFYICONDATA*);  //zwraca indeks istniejącej ikony, jezeli nie istnieje zwraca -1
void AddTrayIcon(NOTIFYICONDATA*);  //dodaje nową ikonę
void ModifyTrayIcon(NOTIFYICONDATA*,int);  //modyfikuje ikonę o podanym indeksie
void DeleteTrayIcon(int);  //usuwa ikonę o podanym indeksie
void DrawTrayIcons(HDC);  //rysuje ikony z traya

int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int nFunsterStil)
{
  if(!CreateTray(nFunsterStil))
  {
    MessageBox(0,"Błąd podczas tworzenia okna tray.","Błąd inicjalizacyjny",MB_ICONERROR|MB_OK);
    return 0;
  }
  SendStartupTray();
  
  MSG messages;
  while(GetMessage(&messages,NULL,0,0))
  {
    TranslateMessage(&messages);
    DispatchMessage(&messages);
  }
  
  for(int i=0;i<TrayIcons.size();i++) delete TrayIcons[i];
  TrayIcons.clear();
  
  return messages.wParam;
}


LRESULT CALLBACK WindowProcedure(HWND hwnd,UINT message,WPARAM wParam,LPARAM lParam)
{
  switch(message)
  {
    case WM_COPYDATA:  //obsługa komunikatu zewnetrznego(traya)
      TrayMessage((COPYDATASTRUCT*)lParam);  //obsługa komunikatu
      break;
    
    case WM_PAINT://rysowanie ikon
      {
        PAINTSTRUCT ps;
        BeginPaint(hwnd,&ps);
        DrawTrayIcons(ps.hdc);
        EndPaint(hwnd,&ps);
      }
      break;
    
    case WM_DESTROY:
      PostQuitMessage (0);
      break;
    
    default:
      return DefWindowProc (hwnd,message,wParam,lParam);
  }
  return 0;
}


void TrayMessage(COPYDATASTRUCT* cpData)
{
  DWORD trayCommand;  //polecenie traya, np. NIM_ADD, NIM_MODIFY, NIM_DELETE
  NOTIFYICONDATA* iconData;  //wskaźnik na NOTIFYICONDATA
  if(cpData->dwData==1)
  {
    trayCommand=*(DWORD*)(((BYTE*)cpData->lpData)+4);//wyciągnięcie polecenia
    iconData=(NOTIFYICONDATA*)(((BYTE*)cpData->lpData)+8);//wyciągnięcie wskaźnika na NOTIFYICONDATA
    int index=FindTrayIcon(iconData);//szukanie ikony w istniejących ikonach
    switch(trayCommand)
    {
      case NIM_ADD://polecenie dodania ikony
        if(index==-1) AddTrayIcon(iconData);//jesli nie nieznaleziono(-1) dodajemy nową ikonę
        break;
      case NIM_MODIFY://polecenie modyfikacji ikony
        if(index!=-1) ModifyTrayIcon(iconData,index);//jeśli znaleziono modyfikujamy
        break;
      case NIM_DELETE://polecenie usunięcia ikony
        if(index!=-1) DeleteTrayIcon(index);//jeśli znaleziono usunięcie
        break;
    }
  }
}


BOOL CALLBACK EnumWindowsProcTray(HWND hwnd,LPARAM message)//procedura wysyłająca do każdego okna komunikat
{
  PostMessage(hwnd,message,0,0);
  return 1;
}

void SendStartupTray()
{
  UINT WM_TASKBARCREATED=RegisterWindowMessage("TaskbarCreated");  //komunikat gotowości traya
  EnumWindows(EnumWindowsProcTray,WM_TASKBARCREATED);  //wysyłanie go wszystkich okien
}


bool CreateTray(int show)
{
  WNDCLASSEX wincl;
  char szTrayString[]="Shell_TrayWnd";
  
  wincl.hInstance=GetModuleHandle(0);
  wincl.lpszClassName=szTrayString;
  wincl.lpfnWndProc=WindowProcedure;
  wincl.style=0;
  wincl.cbSize=sizeof(WNDCLASSEX);
  wincl.hIcon=0;
  wincl.hIconSm=0;
  wincl.hCursor=LoadCursor(NULL,IDC_ARROW);
  wincl.lpszMenuName=0;
  wincl.cbClsExtra=0;
  wincl.cbWndExtra=0;
  wincl.hbrBackground=(HBRUSH)COLOR_BACKGROUND;
  if(!RegisterClassEx(&wincl)) return 0;
  
  hWnd=CreateWindowEx(WS_EX_TOPMOST,szTrayString,"Okno traya :>",WS_OVERLAPPEDWINDOW,
                           CW_USEDEFAULT,CW_USEDEFAULT,544,70,HWND_DESKTOP,NULL,
                           GetModuleHandle(0),NULL);
  if(!hWnd) return 0;
  ShowWindow(hWnd,show);
  return 1;
}

// FUNKCJE DO OPEROWANIA TRAYEM

int FindTrayIcon(NOTIFYICONDATA* iconData)
{
  DWORD size=TrayIcons.size();  //ilość istniejących ikon(wielkość vectora)
  //szuka czy w vectorze nie ma już takiej ikony(sprawdza okno właściciela i numer identyfikacyjny ikony)
  for(DWORD i=0;i<size;i++) if((iconData->hWnd==TrayIcons[i]->hwnd)&&(iconData->uID==TrayIcons[i]->uID)) return i;
  return -1;
}

void AddTrayIcon(NOTIFYICONDATA* iconData)
{
  STrayIcon* TIcon=new STrayIcon;  //stworzenie nowej struktury na ikonę
  if(iconData->hWnd) TIcon->hwnd=iconData->hWnd;  //zapisanie właściciela ikony
  else
  {
    delete TIcon;
    return;
  }
  TIcon->uID=iconData->uID;  //zapisanie identyfikatora
  if(iconData->uFlags&NIF_ICON) //jeśli mamy ikonę
  {
    if(iconData->hIcon) TIcon->hIcon=iconData->hIcon; //zapisujemy jej uchwyt
    else  //a jeżeli nie to nie zapisujemy ikony(w praktyce niektóre programy dodają ikony bez ikon :>)
    {
      delete TIcon;
      return;
    }
  }
  else  //jeżeli nie mamy ikony to też nie zapisujemy ikony
  {
    delete TIcon;
    return;
  }
  if(iconData->uFlags&NIF_MESSAGE) TIcon->uMessage=iconData->uCallbackMessage;  //jeśli mamy komunikat zwrotny
  else TIcon->uMessage=0;//jeżeli nei mamy
  TrayIcons.push_back(TIcon);//umieszczamy strukturę ikony w vectorze
  //odrysowujemy okno
  InvalidateRect(hWnd,0,1);
  UpdateWindow(hWnd);
}

void ModifyTrayIcon(NOTIFYICONDATA* iconData,int index)
{
  bool change=0;//czy zaszły jakieś zmiany
  if(iconData->uFlags&NIF_ICON)
  {
    if(iconData->hIcon)
    {
      if(iconData->hIcon!=TrayIcons[index]->hIcon)
      {
        TrayIcons[index]->hIcon=iconData->hIcon;
        change=1;  //zaszły
      }
    }
  }
  if(iconData->uFlags&NIF_MESSAGE)
  {
    if(iconData->uCallbackMessage!=TrayIcons[index]->uMessage)
    {
      TrayIcons[index]->uMessage=iconData->uCallbackMessage;
      change=1;  //zaszły
    }
  }
  if(change)   //jeśli zaszły zmiany odrysowujemy
  {
    InvalidateRect(hWnd,0,1);
    UpdateWindow(hWnd);
  }
}

void DeleteTrayIcon(int index)
{
  vector<STrayIcon*>::iterator i=TrayIcons.begin();
  for(DWORD a=0;a<index;a++) i++;
  delete *i;
  TrayIcons.erase(i);
  InvalidateRect(hWnd,0,1);
  UpdateWindow(hWnd);
}

void DrawTrayIcons(HDC hdc)
{
  int pos;
  for(int i=0;i<TrayIcons.size();i++)//po koleji każdą ikonę w vecotrze
  {
    pos=10+i*18;//pozycja ikony
    DrawIconEx(hdc,pos,10,TrayIcons[i]->hIcon,16,16,0,0,DI_NORMAL);
  }
}

Jak widać na obrazku na naszym tray’u nie ma tyle ikon co na explorerowym, a dokładniej rzecz biorąc nie ma systemowych ikon. Dla przykładu na naszym tray’u nie będzie ikonki połączenia z Internetem. Aby była należy załadować odpowiedni obiekt COM, który ma swój wpis w rejestrze. Szczerze mówiąc już nie pamiętam gdzie jest ten wpis :>
W każdym bądź razie życzę udanych programów…

3 komentarzy

Fajny art. Można prosić w Delphi? :)

zobacz sobie SharpEnviro źródła: http://sourceforge.net/projects/sharpe/ screeny i opis http://www.sharpe-shell.org/news.php - jest to shell dla windowsa całkowicie napisany w delphi. Bardzo fajny i brawa dla jego twórców

Za słabo znam Delphi, żeby to przerzucić na Delphi, ale może ktoś będzie tak łaskawy :>