Spy++原理初探

 

Spy++原理初探



下載源代碼

摘要:用Visual Studio搞開發的朋友對Spy++這個工具一定不陌生,它可以分析窗體結構、進程和窗口消息,對開發工作有很大輔助作用。我們需要研究某個對象時,只要調出其查找窗口,拖動探測器的指針到指定窗口/控件上釋放即可。下面,筆者就和大家一起,用VC打造一個屬於自己的Spy++。

關鍵字:句柄 消息 子類化

正文:

  打開VC集成開發環境,建立一個基於對話框的工程。我們把這個工程取名爲SpyXX。在窗體中畫上一個圖片框控件(Picture)、一個靜態文本控件(Static)、兩個複選框控件(Check Box)和一個選項卡控件(Tab Control)。界面設計如下圖。

  
  探測器的製作需要兩個圖標文件(.ico)和一個鼠標光標文件(.cur),分別用於正常狀態下的顯示、鼠標拖出時的顯示以及拖出時的鼠標指針;這些資源哪裏來啊?Spy++中就有啊,用eXeScope挖一下吧。(我是從其他軟件中挖出來的,名字好像叫超級什麼霸,記不太清了,呵呵。)選項卡控件定義5個標籤頁,分別爲"常規"、"樣式"、"類"、"窗口"和"消息"。每個標籤頁的內容用一個屬性頁(Property Page)對話框來製作。下面,我們按照順序描述一下開發過程。
  

  一、探測器的製作
  探測器用一個圖片框控件來顯示,正常狀態下顯示一幅有靶的圖標。當鼠標在上面按下時,顯示內容立刻換爲另一幅無靶的圖標,同時鼠標指針變爲靶狀。這樣,就給人一種靶心被拖出去的感覺了。通過上面的敘述,我們瞭解到圖片框需要響應WM_LBUTTONDOWN消息和WM_LBUTTONUP消息。而圖片框在正常狀態下只響應鼠標單擊消息BN_CLICK。所以,我們要通過子類化來響應上述兩個消息。
  把圖片框的ID設爲IDC_PIC,並選中其Notify屬性(否則不響應消息)。依次點擊菜單Insert->New Class,Class type選擇MFC Class,類名取爲CMyPic,基類爲CStatic。添加CSpyXXDlg類的私有成員變量CMyPic m_pic,在對話框的初始化過程中將其與圖片框關聯。代碼如下:

BOOL CSpyXXDlg::OnInitDialog()

{

    CDialog::OnInitDialog();

    m_pic.SubclassDlgItem(IDC_PIC,this);

    ……

    return TRUE;

}

  在CMyPic類中,我們就可以響應鼠標左鍵按下和彈起的消息了。按Ctrl + W打開Class Wizard,選擇Message Maps標籤頁,在Class name下拉列表中選擇CMyPic。從Messages列表中分別增加WM_LBUTTONDOWN和WM_LBUTTONUP消息,並接受其缺省函數名OnLButtonDown和OnLButtonUp。圖標交換和鼠標光標交換的代碼如下:

void CMyPic::OnLButtonDown(UINT nFlags, CPoint point)

{

    // TODO: Add your message handler code here and/or call default

    SetCapture();   //鼠標捕獲

    HCURSOR hc = LoadCursor(AfxGetApp()->m_hInstance, MAKEINTRESOURCE (IDC_CURSOR1));

    //IDC_CURSOR1是靶形光標資源號

    ::SetCursor(hc);

    HICON hicon2 = LoadIcon(AfxGetApp()->m_hInstance, MAKEINTRESOURCE (IDI_ICON2));

    //IDI_ICON2爲無靶圖標資源號

    this->SetIcon(hicon2);

    CStatic::OnLButtonDown(nFlags, point);

}

void CMyPic::OnLButtonUp(UINT nFlags, CPoint point)

{

    // TODO: Add your message handler code here and/or call default

    ReleaseCapture(); //釋放鼠標捕獲

    HICON hicon1 = LoadIcon(AfxGetApp()->m_hInstance, MAKEINTRESOURCE (IDI_ICON1));

    //IDI_ICON1是有靶圖標資源號

    this->SetIcon(hicon1);

    CStatic::OnLButtonUp(nFlags, point);

}

  探測器外觀製作完成了。可以先運行一下,把鼠標按下後拖動試試。下面來實現其功能:獲取窗口句柄。根據鼠標位置來確定窗口需要用到API函數GetCursorPos和WindowFromPoint。此外,我們還想做到像抓圖程序那樣,鼠標移動到的地方,窗口四周會出現閃爍的矩形。這一點,我們用定時器來實現。定時器設在CSpyXXDlg類中,但要由CMyPic中的OnLButtonUp來啓動。所以,我們定義一個全局變量g_hMe將CSpyXXDlg的實例句柄保存起來。同時,被選取的窗口句柄也涉及到在多個標籤頁中顯示,所以也用全局變量g_hWnd將之保存。其餘的用於顯示標籤頁的屬性頁對話框句柄分別用g_hPage0、g_hPage1、g_hPage2、g_hPage3和g_hPage4來保存。啓動定時器的代碼如下:

FromHandle(g_hMe)->SetTimer(1,600,NULL);

  在定時器中,我們要實現桌面範圍內的矩形繪製。代碼如下:

POINT pnt;

RECT rc;

HWND DeskHwnd = ::GetDesktopWindow(); //取得桌面句柄

HDC DeskDC = ::GetWindowDC(DeskHwnd); //取得桌面設備場景

int oldRop2 = SetROP2(DeskDC, R2_NOTXORPEN);

::GetCursorPos(&pnt); //取得鼠標座標

HWND UnHwnd = ::WindowFromPoint(pnt) ; //取得鼠標指針處窗口句柄

g_hWnd=UnHwnd;

::GetWindowRect(g_hWnd, &rc); //獲得窗口矩形

if( rc.left < 0 ) rc.left = 0;

if (rc.top < 0 ) rc.top = 0;

HPEN newPen = ::CreatePen(0, 3, 0); //建立新畫筆,載入DeskDC

HGDIOBJ oldPen = ::SelectObject(DeskDC, newPen);

::Rectangle(DeskDC, rc.left, rc.top, rc.right, rc.bottom); //在窗口周圍顯示閃爍矩形

Sleep(400); //設置閃爍時間間隔

::Rectangle( DeskDC, rc.left, rc.top, rc.right, rc.bottom);

::SetROP2(DeskDC, oldRop2);

::SelectObject( DeskDC, oldPen);

::DeleteObject(newPen);

::ReleaseDC( DeskHwnd, DeskDC);

DeskDC = NULL;

  到此,探測器功能全部完成。

  二、兩個複選框
  第一個複選框是"總在最上面",代碼如下:

void CSpyXXDlg::OnChktop()

{

    int nTop=((CButton*)GetDlgItem(IDC_CHKTOP))->GetCheck();

    if(nTop==1)

        :: SetWindowPos(m_hWnd,HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE|SWP_NOSIZE);

    else

        ::SetWindowPos(m_hWnd,HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE|SWP_NOSIZE);

}

  第二個複選框是"16進制"。因爲其值影響到多個屬性頁對話框的內容,所以,也用一全局變量g_nHex保存之:

void CSpyXXDlg::OnChkhex()

{

    g_nHex=((CButton*)GetDlgItem(IDC_CHKHEX))->GetCheck();

}

  這裏,我們還建立了一個全局函數Display,來輸出16進制和10進制時的句柄值:

CString Display(int nVal)

{

    CString str;

    if(g_nHex==1)

    {

        str.Format("%x",nVal);

        str.MakeUpper();

    }

    else

        str.Format("%d",nVal);

    return str;

}

  三、選項卡控件
  選項卡控件中,5個標籤頁對應5個屬性頁對話框,與它們關聯的類分別取名爲CPage0、CPage1、CPage2、CPage3、CPage4。在CSpyXXDlg中建立私有成員變量m_page0、m_page1、m_page2、m_page3、m_page4。在其初始化過程中建立這5個屬性頁對話框:

    m_page0.Create(IDD_OLE_PROPPAGE_LARGE,GetDlgItem(IDC_TAB1));

    m_page1.Create(IDD_OLE_PROPPAGE_LARGE1,GetDlgItem(IDC_TAB1));

    m_page2.Create(IDD_OLE_PROPPAGE_LARGE2,GetDlgItem(IDC_TAB1));

    m_page3.Create(IDD_OLE_PROPPAGE_LARGE3,GetDlgItem(IDC_TAB1));

    m_page4.Create(IDD_OLE_PROPPAGE_LARGE4,GetDlgItem(IDC_TAB1));

    CRect rs;

    m_tab.GetClientRect(rs);

    rs.top+=20;

    rs.bottom-=3;

    rs.left+=3;

    rs.right-=3;

    m_page0.MoveWindow(rs);

    m_page1.MoveWindow(rs);

    m_page2.MoveWindow(rs);

    m_page3.MoveWindow(rs);

    m_page4.MoveWindow(rs);

    m_page0.ShowWindow(SW_SHOW);

    m_tab.SetCurSel(0);

   然後在選項卡消息TCN_SELCHANGE響應函數中控制它們的顯示:

void CSpyXXDlg::OnSelchangeTab1(NMHDR* pNMHDR, LRESULT* pResult)

{

    // TODO: Add your control notification handler code here

    int i=m_tab.GetCurSel();

    switch(i)

        {

    case 0:

        m_page0.ShowWindow(SW_SHOW);

               m_page1.ShowWindow(SW_HIDE);

               m_page2.ShowWindow(SW_HIDE);

               m_page3.ShowWindow(SW_HIDE);

               m_page4.ShowWindow(SW_HIDE);

               break;

    case 1:

               m_page0.ShowWindow(SW_HIDE);

               m_page1.ShowWindow(SW_SHOW);

               m_page2.ShowWindow(SW_HIDE);

               m_page3.ShowWindow(SW_HIDE);

               m_page4.ShowWindow(SW_HIDE);

               break;

    case 2:

        ……

    default:

        ;

    }

    *pResult = 0;

}

  四、常規標籤頁
  常規標籤頁負責顯示窗口句柄、窗口類名、標題文本、窗口矩形、窗口ID、進程ID和程序路徑。控制其顯示或改變應在CMyPic的WM_LBUTTONUP響應函函數中進行。代碼如下:

((CPage0*)FromHandle(g_hPage0))->m_editHWND.SetWindowText(Display((int)g_hWnd));

char strClass[200]="\0";

::GetClassName(g_hWnd,strClass,200);

((CPage0*)FromHandle(g_hPage0))->m_editCLASS.SetWindowText(strClass);

((CPage2*)FromHandle(g_hPage2))->SetDlgItemText(IDC_EDITCLASSNAME,strClass);<

       

char strTitle[200]="\0";

 ::GetWindowText(g_hWnd,strTitle,200);

((CPage0*)FromHandle(g_hPage0))->m_editTITLE.SetWindowText (strTitle);

long iWNDID=GetWindowLong(g_hWnd,GWL_ID);

((CPage0*)FromHandle(g_hPage0))->m_editWNDID.SetWindowText(Display((int)iWNDID));

      

unsigned long iPID=0;

GetWindowThreadProcessId(g_hWnd,&iPID);

((CPage0*)FromHandle(g_hPage0))->m_editPID.SetWindowText(Display((int)iPID));

       

CString strPath;

strPath=getProcPath(iPID);

((CPage0*)FromHandle(g_hPage0))->m_editPATH.SetWindowText(strPath);

       

RECT rc;

::GetWindowRect(g_hWnd, &rc); //獲得窗口矩形

CString strRect;

strRect.Format("(%d,%d),(%d,%d) %dx%d",rc.left,rc.top,rc.right,rc.bottom,

rc.right-rc.left,rc.bottom-rc.top);

((CPage0*)FromHandle(g_hPage0))->m_editRECT.SetWindowText(strRect);

  其中,getProcPath是獲取進程文件路徑的函數。獲取進程路徑的方法有兩種。在NT系統中,我們可以用OpenProcess()函數將進程打開後,再利用EnumProcessModules()函數枚舉該進程的模塊,最後利用GetModuleFileNameEx()函數就能取得該進程的路徑;第二種方法是利用ToolHelp API中的相關函數。而後者兼容容Windows9x和NT4.0以後系統,所以採取此法。它的實現代碼如下:

CString getProcPath(int PID)

{

    HANDLE hModule;

    MODULEENTRY32* minfo=new MODULEENTRY32;

    minfo->dwSize=sizeof(MODULEENTRY32);

    hModule=CreateToolhelp32Snapshot(TH32CS_SNAPMODULE,PID); Module32First(hModule,minfo);

    CString str;

    str.Format("%s",minfo->szExePath);

    CloseHandle(hModule);

    if(minfo) delete minfo;

    return str;

}

  

  五、樣式標籤頁
  樣式標籤頁設計如下圖:

  

  API函數GetWindowLong可以獲取窗口樣式或擴展樣式的值。然後我們羅列出以WS_開頭的所有窗口樣式與上述樣式值做"位與"操作,如果被包含,則返回其窗口樣式,否則返回0。這樣,就可以得到窗口樣式的列表了。擴展樣式列表與樣式列表類似。相關代碼如下:

CListBox* pListStyle=(CListBox*)(((CPage1*)FromHandle(g_hPage1))->GetDlgItem(IDC_LIST_STYLE));

CListBox* pListExStyle=(CListBox*)(((CPage1*)FromHandle(g_hPage1))->GetDlgItem(IDC_LIST_EX_STYLE));

CEdit* pEditStyle=(CEdit*)(((CPage1*)FromHandle(g_hPage1))->GetDlgItem(IDC_EDIT_STYLE));

CEdit* pEditExStyle=(CEdit*)(((CPage1*)FromHandle(g_hPage1))->GetDlgItem(IDC_EDIT_EX_STYLE));

long style = GetWindowLong(g_hWnd, GWL_STYLE);

long styleEx= GetWindowLong(g_hWnd, GWL_EXSTYLE);

pEditStyle->SetWindowText(Display((int)style));

pEditExStyle->SetWindowText(Display((int)styleEx));

pListStyle->ResetContent(); //清空樣式列表框

pListExStyle->ResetContent(); //清空擴展樣式列表框

if (style & WS_BORDER)

    pListStyle->AddString("WS_BORDER");

if( style & WS_CAPTION)

    pListStyle->AddString("WS_CAPTION");

if( style & WS_CHILD)

    pListStyle->AddString("WS_CHILD");

    ……

  

  六、類標籤頁
  類標籤頁的設計如下圖:

  

  類名在常規標籤頁已獲取。API函數GetClassLong可以獲取類樣式值。樣式列表的實現與窗口樣式類似,不再贅述。
  

  七、窗口標籤頁
  窗口標籤頁的設計如下圖:

  

  在該頁中,主要用到了下面幾個API函數:GetNextWindow、GetWindow和SendMessage。這三個API函數搭配以不同的參數值可以實現不同的功能。這裏沒有用GetWIndowText函數,是因爲它不能取出部分系統窗口和隱藏窗口的標題。我們用SendMessage函數加WM_GETTEXT參數取代之。代碼如下:

CPage3* pPage3=(CPage3*)FromHandle(g_hPage3);

HWND tempHandle;

char tempstr[255]="\0";

tempHandle = g_hWnd; //本窗口句柄

pPage3->SetDlgItemText(IDC_MYHWND, Display((int)tempHandle));

//獲取本窗口標題

::SendMessage(tempHandle, WM_GETTEXT, 255, (LPARAM)tempstr);

pPage3->SetDlgItemText(IDC_MYTITLE, tempstr);

//上一窗口

tempHandle = ::GetNextWindow(g_hWnd, GW_HWNDPREV);

pPage3->SetDlgItemText(IDC_PREHWND, Display((int)tempHandle));

//獲取上一窗口標題

memset(tempstr,0,255);

::SendMessage(tempHandle, WM_GETTEXT, 255, (LPARAM)tempstr);

pPage3->SetDlgItemText(IDC_PRETITLE, tempstr);

//下一窗口

tempHandle = ::GetNextWindow(g_hWnd, GW_HWNDNEXT);

pPage3->SetDlgItemText(IDC_NEXTHWND,Display((int)tempHandle));

memset(tempstr,0,255); //獲取下一窗口標題

::SendMessage(tempHandle, WM_GETTEXT, 255, (LPARAM)tempstr);

pPage3->SetDlgItemText(IDC_NEXTTITLE, tempstr);

       

tempHandle = ::GetParent(g_hWnd); //父窗口

pPage3->SetDlgItemText(IDC_PARENTHWND, Display((int)tempHandle));

memset(tempstr,0,255);

::SendMessage(tempHandle, WM_GETTEXT, 255, (LPARAM)tempstr);

pPage3->SetDlgItemText(IDC_PARENTTITLE,tempstr);

//第一子窗口

tempHandle = ::GetWindow(g_hWnd, GW_CHILD);

pPage3->SetDlgItemText(IDC_CHILDHWND,Display((int)tempHandle));

memset(tempstr,-0,255);

::SendMessage(tempHandle, WM_GETTEXT, 255, (LPARAM)tempstr);

pPage3->SetDlgItemText(IDC_CHILDTITLE,tempstr);

//所有者窗口

tempHandle = ::GetWindow(g_hWnd, GW_OWNER);

Page3->SetDlgItemText(IDC_OWNERHWND,Display((int)tempHandle));

memset(tempstr,0,255);

::SendMessage(tempHandle, WM_GETTEXT, 255, (LPARAM)tempstr);

pPage3->SetDlgItemText(IDC_OWNERTITLE, tempstr);

  

  八、消息標籤頁

  消息標籤頁的設計如下圖:

  
該頁中的列表框與樣式列表框不同,它的每個列表項前都有一個複選框。這要用到類CCheckListBox。這裏要再次用到子類化的知識。從本文第一段製作CMyPric過程中,我們體會到了子類化的作用,也感到了它的不便之處。這裏,我們採取另外一種方法,借雞生蛋:即用Class Wizard生成相關代碼,然後再修改它。首先在該屬性頁對話框上畫一個列表控件,打開Class Wizard關聯一個CListBox類變量m_listStatus。設置列表框的Owner Draw屬性爲Fixed,並選中其Has Strings選項。如下圖:

      

  然後,在Page4.h中查找到m_listStatus的定義 CListBox m_listStatus並將其改爲CCheckListBox m_listStatus。這樣,我們就可以使用CCheckListBox的全部函數了。
  在對話框初始化過程中添加下列語句以加入各列表項:

CCheckListBox* plistStatus=((CCheckListBox*)FromHandle(g_hPage4)->GetDlgItem(IDC_LISTSTATUS));

plistStatus->AddString("窗口可見");

plistStatus->AddString("窗口可用");

plistStatus->AddString("總在最前");

plistStatus->AddString("窗口只讀");

plistStatus->AddString("最大化");

plistStatus->AddString("最小化");

plistStatus->AddString("窗口還原");

plistStatus->AddString("關閉窗口");

plistStatus->AddString("激活窗口");

  接下來我們要判斷,當窗口/控件被選定後,哪些列表項被勾選。這個判斷過程與樣式列表的實現類似。如第一項"窗口可見",代碼如下:

long style = GetWindowLong(g_hWnd, GWL_STYLE);

if( style & WS_VISIBLE )

{

    pListStatus->SetCheck(0,1);

}

  其餘各項詳見源代碼。 這個列表框的作用不僅僅是顯示窗口的狀態,還要在發生勾選改動時即時改變窗口狀態或激發其行爲。勾選狀態改變的消息是LBN_SELCHANGE。另外,爲了不使一個勾選的改變就引起所有列表項都激發一遍,我們採用switch結構,以使哪個列表項被選中就激發哪個列表項。代碼如下:

void CPage4::OnSelchangeListstatus()

{

    // TODO: Add your control notification handler code here

    int n=m_listStatus.GetCurSel();

    switch(n)

    {

    case 0:

        if(m_listStatus.GetCheck(0)== 1 )

            ::ShowWindow(g_hWnd, SW_SHOW);

        else

            ::ShowWindow(g_hWnd, SW_HIDE);

        break;

    case 1:

        if(m_listStatus.GetCheck(1) == 1)

            ::EnableWindow(g_hWnd, TRUE);

        else

            ::EnableWindow(g_hWnd,FALSE);

        break;

    case 2:

        if(m_listStatus.GetCheck(2) == 1)

            ::SetWindowPos(g_hWnd,HWND_TOPMOST,0,0,0,0,SWP_NOMOVE | SWP_NOSIZE);

        else

            ::SetWindowPos(g_hWnd,HWND_NOTOPMOST,0,0,0,0,SWP_NOMOVE|SWP_NOSIZE);

        break;

    case 3:

        if(m_listStatus.GetCheck(3) == 1)

            ::SendMessage(g_hWnd, EM_SETREADONLY, TRUE, 0);

        else

            ::SendMessage(g_hWnd, EM_SETREADONLY, FALSE, 0);

        break;

    case 4:

        if(m_listStatus.GetCheck(4) ==1)

        {

            ::ShowWindow(g_hWnd, SW_MAXIMIZE);

            m_listStatus.SetCheck(5,0);

        }

        else

            ::ShowWindow (g_hWnd, SW_RESTORE);

        break;

    case 5:

        if (m_listStatus.GetCheck(5) == 1)

        {

            ::ShowWindow(g_hWnd, SW_MINIMIZE);

            m_listStatus.SetCheck(4,0);

        }

        else

            ::ShowWindow(g_hWnd, SW_RESTORE);

        break;

    case 6:

        if(m_listStatus.GetCheck(6) ==1)

        {

            ::ShowWindow (g_hWnd, SW_RESTORE);

            m_listStatus.SetCheck(6,0);

            m_listStatus.SetCheck(5,0);

            m_listStatus.SetCheck(4,0);

        }

        break;

    case 7:

        if(m_listStatus.GetCheck(7) ==1)

        {

            ::SendMessage (g_hWnd, WM_CLOSE, 0, 0);

            m_listStatus.SetCheck(7,0);

        }

        break;

    case 8:

        if(m_listStatus.GetCheck(8) ==1)

        {

            ::BringWindowToTop(g_hWnd);

            m_listStatus.SetCheck(8,0);

        }

        break;

    default:

    ;

    }

}

  Spy++打造完畢。回顧其過程,難點不多,細細碎碎問題不少。也難免啊,不僅要形似,咱還要神似。文中一定還有很多地方不夠周全,希望同行朋友們不吝賜教。代碼在Window XP + VC6.0中調試通過。Spy++源碼同時放在這裏。歡迎訪問我的個人主頁(阿珊境界)http://www.asanscape.com,歡迎加入我們的VC討論羣713035。

發佈了6 篇原創文章 · 獲贊 11 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章