0x20FF018B

原文地址:http://www.fengyuan.com/article/wmprint.html

Problem

The normal way of capturing the contents of a window into a bitmap is creating a memory device context (CreateCompatibleDC), creating a device-dependent bitmap (CreateCompatibleBitmap) or a DIB section (CreateDIBSection), selecting the bitmap into the memory DC (SelectObject), and then bitblt from the window's device context (GetWindowDC) into the memory DC (Bitblt). After that, a copy of the contents of the window as it appears on the screen is stored in the bitmap.

But what if the window is hidden, or partially blocked with other windows ? When a window is hidden or partially blocked, the non-visible part of the window will be clipped in the device context returned from GetWindowDC. In other words, that part of the window can't be captured using a simple BitBlt.

To capture any window, completely visible, partially visible, or complete invisible, Win32 API provides two special messages, WM_PRINT and WM_PRINTCLIENT. Both these messages take a device context handle as a parameter, and the window handling these messages is supposed to draw itself or its client area into the device context provided.

Sounds good ? There is a catch. Normally only windows implemented by the operating system are knowledgeable to handle these messages. If you send a WM_PRINT message to a window, normally all the non-client area, which includes border, title bar, menu bar, scroll bar, etc., and common controls are drawn properly. Client area of windows implemented by application programs are normally left blank.

This article shows a sophisticated method to trick a window implemented by applications to handle WM_PRINTCLIENT messagewithout its source code.

Test Program

To experiment with WM_PRINT/WM_PRINTCLIENT messages, a text program is written. Actually, the program is a slightly modified version of the "Hello, World" program generated by MSVC Wizard (plain Win32 application).

The routine handling WM_PAINT message is OnPaint, which calls BeginPaint, a custom drawing routine OnDraw, and then EndPaint. The OnDraw routine just draws an ellipse in the client area. Here is how its screen looks.

.

void OnDraw(HWND hWnd, HDC hDC)
{
    RECT rt;
    GetClientRect(hWnd, &rt);

    SelectObject(hDC, GetSysColorBrush(COLOR_INFOBK));
    Ellipse(hDC, rt.left+5, rt.top+5, rt.right-3, rt.bottom-5);
}

void OnPaint(HWND hWnd)
{
    PAINTSTRUCT ps;
	
    HDC hDC = BeginPaint(hWnd, & ps);

    OnDraw(hWnd, hDC);
    EndPaint(hWnd, & ps);
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message) 
    {
	case WM_COMMAND:
	    switch ( LOWORD(wParam) )
	    {
		case IDM_EXIT:
		    DestroyWindow(hWnd);
		    break;

                case ID_FILE_PRINT:
                    PrintWindow(hWnd);
                    break;

		default:
		   return DefWindowProc(hWnd, message, wParam, lParam);
	    }
	    break;

	case WM_PAINT:
            OnPaint(hWnd);
            break;
		
        case WM_DESTROY:
	    PostQuitMessage(0);
	    break;

        default:
   	    return DefWindowProc(hWnd, message, wParam, lParam);
   }

   return 0;
}

To test the WM_PRINT/WM_PRINTCLIENT message, a "Print" menu item is added to the "File" menu. The window procedure calls the following PrintWindow routine to capture the screen using WM_PRINT message. Note the WM_PRINT message creates a memory DC, a DDB, selects the DDB into the memory DC, and then passes the memory DC handle as the WPARAM of the WM_PRINT message. The LPARAM parameter of the message specifies that everything should be drawn, including client/non-client area, background, and any child window.

void PrintWindow(HWND hWnd)
{
    HDC hDCMem = CreateCompatibleDC(NULL);

    RECT rect;

    GetWindowRect(hWnd, & rect);

    HBITMAP hBmp = NULL;

    {
        HDC hDC = GetDC(hWnd);
        hBmp = CreateCompatibleBitmap(hDC, rect.right - rect.left, rect.bottom - rect.top);
        ReleaseDC(hWnd, hDC);
    }

    HGDIOBJ hOld = SelectObject(hDCMem, hBmp);
    SendMessage(hWnd, WM_PRINT, (WPARAM) hDCMem, PRF_CHILDREN | PRF_CLIENT | PRF_ERASEBKGND | PRF_NONCLIENT | PRF_OWNED);

    SelectObject(hDCMem, hOld);
    DeleteObject(hDCMem);

    OpenClipboard(hWnd);

    EmptyClipboard(); 
    SetClipboardData(CF_BITMAP, hBmp);
    CloseClipboard();
}

When WM_PRINT message returns, the bitmap is pasted to the clipboard, so you can use any graphics application to view/save the image. Here is what's being captured, everything except the ellipse.

Prototype Solution

The problem with the test program shown above is of course the disconnection between the WM_PRINT message, and the OnPaint routine handling WM_PAINT message. The default window procedure is smart enough to draw non-client area and then send a WM_PRINTCLIENT message to the window. But there is no default processing for the WM_PRINTCLIENT message, which explains why only the client are is left blank.

If you have the source code of the window procedure, adding a handling of WM_PRINTCLIENT to share the WM_PAINT message handling is very easy, as is shown below.

void OnPaint(HWND hWnd, WPARAM wParam)
{
    PAINTSTRUCT ps;
    HDC         hDC;

    if ( wParam==0 )
        hDC = BeginPaint(hWnd, & ps);
    else
        hDC = (HDC) wParam;

    OnDraw(hWnd, hDC);

    if ( wParam==0 )
        EndPaint(hWnd, & ps);
}

LRESULT CALLBACK WndProc0(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
        ...
        case WM_PRINTCLIENT:
            SendMessage(hWnd, WM_PAINT, wParam, lParam);
            break;

	case WM_PAINT:
            OnPaint(hWnd, wParam);
    	break;
	...
   }
   return 0;
}

In the code shown above, handling for the WM_PRINTCLIENT is added, which just sends a WM_PAINT message to the window itself with the wParam and lParam. The WPARAM parameter is added to OnPaint routine. When wParam is not 0, it's cast into a device context handle, instead of calling BeginPaint to retrive a device context for the non-client area. Likewise, EndPaint is only called when wParam is 0. These simple changes in source code level makes the whole WM_PRINT message handling complete for client area.

Handling WM_PRINTCLIENT Message without Source Code Change

What if you do not have the source code of the window ? Subclassing the window to add a handling for the WM_PRINTCLIENT is easy. What's hard is how to trick bypass BeginPaint and EndPaint, and how to pass the wParam from WM_PRINTCLIENT to the drawing code after BeginPaint.

Here is the declaraction of the CPaintHook class which handles window subclassing and implementation of WM_PRINTCLIENT message handling.

// Copyright (C) 2000 by Feng Yuan (www.fengyuan.com)

class CPaintHook
{
    BYTE      m_thunk[9];
    WNDPROC   m_OldWndProc;
    HDC       m_hDC;

    static HDC  WINAPI MyBeginPaint(HWND hWnd, LPPAINTSTRUCT lpPaint);
    static BOOL WINAPI MyEndPaint(HWND hWnd, LPPAINTSTRUCT lpPaint);

    virtual LRESULT WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
	
public:
    
    bool Within_WM_PRINT(void) const
    {  
        if ( (m_thunk[0]==0xB9) && ((* (unsigned *) (m_thunk+5))==0x20FF018B) )
            return m_hDC !=0;
        else
            return false;
    }

    void SubClass(HWND hWnd);
};

The CPaintHook class has three member variables, one BYTE array of storing some machine code, a pointer to the original window procedure, and a device context handle. The two static methods replaces the original system provided BeginPaint and EndPaint routines. A virtual window message procedure is provided to override message processing for the window. Finally, the SubClass method subclasses an existing window and makes sure it handles WM_PRINTCLIENT message properly.

The implementation of this seemly simple class is quite tricky. Some knowledge of Win32 API implementation, compiler code generation, Intel machine code, and virtual memory is needed to understand it fully.

// Copyright (C) 2000 by Feng Yuan (www.fengyuan.com)

#include "stdafx.h"
#include <assert.h>

#include "hookpaint.h"

bool Hook(const TCHAR * module, const TCHAR * proc, unsigned & syscall_id, BYTE * & pProc, const void * pNewProc)
{
    HINSTANCE hMod = GetModuleHandle(module);

    pProc = (BYTE *) GetProcAddress(hMod, proc);

    if ( pProc[0] == 0xB8 )
    {
        syscall_id = * (unsigned *) (pProc + 1);

        DWORD flOldProtect;

        VirtualProtect(pProc, 5, PAGE_EXECUTE_READWRITE, & flOldProtect);

        pProc[0] = 0xE9;
        * (unsigned *) (pProc+1) = (unsigned)pNewProc - (unsigned) (pProc+5);

        pProc += 5;

        return true;
    }
    else
        return false;
}

The Hook routine hooks certain kind of exported function from a module by directly modifying its starting machine code. The benefit of hacking machine code directly is that you only need to hack into a single place, all the call in a process is taken care of. But hacking machine code directly is very tricky because it's not easy to parse machine code to find extra space for a five byte jump instruction. Chapter 4 of my book contains more generic code to handle this problem. What's shown here only applies to a special case, which applies to BeginPaint and EndPaint on Windows NT/2000 machines. On these machine, BeginPaint and EndPaint calls system services provided byWin32K.SYS. These routines follow a strict pattern, the first instruction stores a DWORD index into the EAX register. The instructions after that issue a software interruption (0x2E), which will be served by Win32K.SYS in kernel mode address space.

The Hook routine uses GetModuleHandle to retrieve module handle, GetProcAddress to retrieve the address of an exported Win32 API function. It then checks if the first instruction is a constant move to EAX register instruction (0xB8). If a match is found, VirtualProtect is used to change the protection flag for that page to PAGE_EXECUTE_READWRITE, which makes it writeable. The system service call index is saved, and then the first five bytes are changed to a jump instruction to a function whose address is passed through the pNewProc parameter.

LRESULT CPaintHook::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    assert(m_OldWndProc);

    if ( uMsg==WM_PRINTCLIENT )
    {
        m_hDC = (HDC) wParam;
        uMsg  = WM_PAINT;
    }

    LRESULT hRslt = CallWindowProc(m_OldWndProc, hWnd, uMsg, wParam, lParam);

    m_hDC = NULL;

    return hRslt;
}

Implementing CPaintHook::WndProc is fairly simple. If the current message is WM_PRINTCLIENT, the device context handle passed in WPARAM is saved in member variable m_hDC, and then message is changed to WM_PAINT. CallWindowProc is used to call the original window procedure.

HDC WINAPI CPaintHook::MyBeginPaint(HWND hWnd, LPPAINTSTRUCT lpPaint)
{
    const CPaintHook * pThis = (CPaintHook *) GetWindowLong(hWnd, GWL_WNDPROC);
    
    pThis = (const CPaintHook *) ( (unsigned) pThis - (unsigned) & pThis->m_thunk[0] + (unsigned) pThis );
    
    if ( pThis->Within_WM_PRINT() )
    {
        memset(lpPaint, 0, sizeof(PAINTSTRUCT));

        lpPaint->hdc = pThis->m_hDC;
        
        GetClientRect(hWnd, & lpPaint->rcPaint);
        
        return pThis->m_hDC;
    }
    else
    {
        __asm   mov     eax, syscall_BeginPaint
        __asm   push    lpPaint
        __asm   push    hWnd
        __asm   call    pBeginPaint
    }
}

BOOL WINAPI CPaintHook::MyEndPaint(HWND hWnd, LPPAINTSTRUCT lpPaint)
{
    const CPaintHook * pThis = (CPaintHook *) GetWindowLong(hWnd, GWL_WNDPROC);
    
    pThis = (const CPaintHook *) ( (unsigned) pThis - (unsigned) & pThis->m_thunk[0] + (unsigned) pThis );
    
    if ( pThis->Within_WM_PRINT() )
        return TRUE;
    else
    {
        __asm   mov     eax, syscall_EndPaint
        __asm   push    lpPaint
        __asm   push    hWnd
        __asm   call    pEndPaint
    }
}

Implementation of the two static functions, MyBeginPaint and MyEndPaint, are very similar. Beging static member functions, they do not have 'this' pointer to access object member variables. The two functions calculates the current 'this' pointer from the current window procedure address, which is the address of its m_thunk member variable (explained below). Once 'this' pointer is got, the m_hDC member variable is changed to see if we're actually handling a WM_PRINTCLIENT message, instead of normal WM_PAINT message. If a device context handle is given, the original BeginPaint and EndPaint will be skipped. Otherwise, the system service index is set into the EAX register, and the instructions after the first instruction in the original BeginPaint/EndPaint is called as a subroutine, although a jump instruction without pusing the parameters will work too.

static unsigned syscall_BeginPaint = 0;
static BYTE *   pBeginPaint        = NULL;

static unsigned syscall_EndPaint   = 0;
static BYTE *   pEndPaint          = NULL;

CPaintHook::CPaintHook()
{
    static bool s_hooked = false;

    if ( ! s_hooked )
    {
        Hook("USER32.DLL", "BeginPaint", syscall_BeginPaint, pBeginPaint, MyBeginPaint);
        Hook("USER32.DLL", "EndPaint",   syscall_EndPaint,   pEndPaint,   MyEndPaint);

        s_hooked = true;
    }

    m_thunk[0]              = 0xB9;	     // mov ecx, 
    *((DWORD *)(m_thunk+1)) = (DWORD) this;  //          this
    *((DWORD *)(m_thunk+5)) = 0x20FF018B;    // mov eax, [ecx] 

    m_OldWndProc = NULL;
    m_hDC        = NULL;
}

void CPaintHook::SubClass(HWND hWnd)
{		
    m_OldWndProc = (WNDPROC) GetWindowLong(hWnd, GWL_WNDPROC);
    SetWindowLong(hWnd, GWL_WNDPROC, (LONG) ((void *) m_thunk));
}

The constructor CPaintHook::CPaintHook and the SubClass method are the magic glue which hold everything together. The constructor will make sure the Hook function are called twice to hook Win32 API function BeginPaint and EndPaint, which are both exported from module USER32.DLL. For each instrance of the CPaintHook class, it's m_thunk data member will be initialized to two machine instructions. The first moves 'this' pointer to the ECX register, the second calls the first virtual method of that object, the CPaintHook::WndProc virtual method implementation.

The SubClass method remembers the original window procedure, and passes the address of m_thunk data member as the new window procedure.

With the CPaintHook class, hooking a window to handle WM_PRINTCLIENT message is a piece of cake. Here is the WinMain function of our test program, which creates an instance of the CPaintHook class on the stack, and calls the SubClass method.

int APIENTRY WinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPSTR     lpCmdLine,
                     int       nCmdShow)
{
    WNDCLASSEX wcex;

    memset(&wcex, 0, sizeof(wcex));
	
    wcex.cbSize         = sizeof(WNDCLASSEX); 
    wcex.style		= CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc    = (WNDPROC) WndProc;
    wcex.hInstance	= hInstance;
    wcex.hCursor	= LoadCursor(NULL, IDC_ARROW);
    wcex.hbrBackground  = (HBRUSH)(COLOR_WINDOW+1);
    wcex.lpszMenuName   = (LPCSTR) IDC_PAINT;
    wcex.lpszClassName  = "Class";

    RegisterClassEx(&wcex);

    HWND hWnd = CreateWindow("Class", "WM_PRINT", WS_OVERLAPPEDWINDOW,
                    CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);

    assert(hWnd);

    CPaintHook hook;

    hook.SubClass(hWnd);

    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);

    MSG msg;
   	
    while (GetMessage(&msg, NULL, 0, 0)) 
    {
	TranslateMessage(&msg);
	DispatchMessage(&msg);
    }

    return msg.wParam;
}

Sample Program

Sample program using WIN32 API wmprint.zip

Sample program for capturing windows of other processes using DLL injection:capture.zip

Limitation

  • The Hook function only handles exported function whose first instruction is "MOV EAX, <DWORD_constant>". So the implementation shown here only applies to Windows NT/2000. Refer to Chapter 4 for more generic of restrictive API hooking solutions.
  • Only tested on Windows 2000 machine
### Selenium自动化测试126邮箱登录失败问题分析与解决方案 在Selenium自动化测试中,126邮箱登录失败的问题可能由多种原因引起。以下是对问题的详细分析和解决方案。 #### 1. 动态ID导致定位元素失败 动态ID是常见的问题之一,因为页面中的元素ID可能会随着加载时间或会话变化而改变。如果直接使用动态生成的ID进行定位,会导致`NoSuchElementException`或其他异常。针对此问题,可以采用以下方法解决: - **使用更稳定的定位方式**:避免直接依赖动态ID,改用其他属性(如`name`、`class`、`XPath`等)来定位元素[^1]。 - **示例代码**: ```python from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC driver = webdriver.Chrome() driver.get("http://www.126.com") # 切换到密码登录模式 switch_to_password_login = WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.XPATH, "//a[contains(text(), '密码登录')]")) ) switch_to_password_login.click() # 定位用户名输入框并输入内容 username_input = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.NAME, "email")) ) username_input.clear() username_input.send_keys("your_username") # 定位密码输入框并输入内容 password_input = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.NAME, "password")) ) password_input.clear() password_input.send_keys("your_password") # 点击登录按钮 login_button = WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.ID, "dologin")) ) login_button.click() ``` #### 2. 页面加载超时或元素未完全加载 如果页面加载较慢或某些元素未完全加载,可能导致Selenium无法正确找到目标元素。可以通过显式等待来解决此问题: - **使用WebDriverWait**:确保在操作元素之前,等待其加载完成[^2]。 - **代码示例**见上一部分。 #### 3. 浏览器兼容性问题 有时,Selenium脚本运行失败可能是由于浏览器驱动版本与浏览器本身不兼容。例如,ChromeDriver版本与Chrome浏览器版本不匹配。 - **解决方案**:确保使用的浏览器驱动版本与浏览器版本一致[^1]。 - **检查版本命令**: ```bash # 检查Chrome浏览器版本 chrome --version # 下载对应版本的ChromeDriver https://chromedriver.chromium.org/downloads ``` #### 4. `Message Stacktrace GetHandleVerifier` 错误 该错误通常与Selenium底层的处理逻辑有关,具体原因可能包括但不限于以下几种: - **未正确关闭浏览器资源**:如果在脚本结束时未调用`driver.quit()`,可能会导致资源泄漏,进而引发类似错误。 - **解决方案**:确保在脚本结束时调用`driver.quit()`释放资源[^1]。 - **代码示例**: ```python try: # 执行登录操作 pass finally: driver.quit() ``` - **其他潜在原因**:可能是由于Selenium或浏览器驱动存在Bug。尝试更新Selenium库和浏览器驱动至最新版本。 - **更新Selenium库**: ```bash pip install --upgrade selenium ``` #### 5. 其他常见问题及解决方法 - **验证码问题**:如果登录过程中需要输入验证码,Selenium脚本无法自动处理。可以通过模拟人工输入或使用第三方服务(如打码平台)解决。 - **多窗口切换问题**:如果登录后跳转到新窗口,需先切换到新窗口再进行后续操作[^1]。 - **代码示例**: ```python # 获取所有窗口句柄 window_handles = driver.window_handles # 切换到最新的窗口 driver.switch_to.window(window_handles[-1]) ``` --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值