发布者: Echo

目的

在wxWidgets GUI开发过程中,经常需要查找某窗口对应的C+ +类,通常做法是根据窗口的标签、行为、父窗口关系等在代码中直接搜索,效率很低。为此开发了一个类似于windows下Spy+ +的小工具。 在GUI程序中按下开关快捷键,开启Spy模式,移动鼠标,会高亮所在窗口,并使用tooltip显示窗口对应C++类名。 wxEventTableEntry

主要思路

wxWidgets提供了wxFindWindowAtPoint函数可以获取位于某坐标的窗口指针,然后使用typeid(object).name()函数获取类名,并显示.

关键点

  • 如何实现不需要重新编译GUI程序的情况下就可以使用该工具
  • 如何实现随鼠标移动对窗口spy
  • 高亮窗口的刷新 

1. 程序HOOK实现

当在UNIX下使用动态链接库时,通过设置LD_PRELOAD可以定义程序运行前优先加载的动态连接库。通过这个环境变量可以实现程序的代码注入。 举个例子:程序A引用了动态连接库lib_real.so中的函数void Add(int a, int b),我们可以定制一个动态链接库lib_inject.so也导出相同签名的Add函数,并设置环境变量LD_PRELOAD="lib_inject.so"。 当运行程序A时,系统会首先加载lib_inject.so,并修改函数导入表中的函数指针指向lib_inject.so中的Add函数。 使用这个方法我们可以实现对应用程序的HOOK. 由于我们目标是linux gtk下使用wxWidgets开发的GUI程序,所以这里就拿gtk的函数开刀:

extern "C" void gtk_main(void)
{
    typedef void (*FuncGtkMain)(void);
    static FuncGtkMain real_gtk_main = NULL;
    if(real_gtk_main == NULL)
    {
        real_gtk_main = (FuncGtkMain)dlsym(RTLD_NEXT, "gtk_main");
    }

    wxMessageBox("hooked successfully");
    init_hook();
    real_gtk_main();
}

代码中首先使用dlsym函数获取真实的gtk_main函数地址,然后执行我们的代码,最后调用真正的gtk_main。 在init_hook函数中我们就可以为所欲为了:

void init_hook()
{
    handler->SetContainer(wxTheApp->GetTopWindow());
    wxTheApp->Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(HotKeyHandler::OnKeyDown), NULL, 		handler);
}

这里我们在app中插入了我们自己的按键事件响应,以响应Spy开关快捷键操作。在HotKeyHandler::OnKeyDown开始Spy:

class HotKeyHandler : public wxEvtHandler
{
public:
    void OnKeyDown(wxKeyEvent& event)
    {
       	if(event.GetKeyCode() == WXK_F7) // 快捷键设为F7
       	{
           	WidgetSpy::StartShowTip(m_container);
       	}
       	else
       	{
           	event.Skip();
       	}
    }
    // ...
};

2. 鼠标HOOK实现

Windows下有钩子可以实现鼠标的HOOK,但是linux没有,不过wxWindow::CaptureMouse函数也可以满足我们的需要,在这里创建一个隐藏的wxWindow,用于捕获鼠标消息:

void WidgetSpy::StartSpy(wxWindow *parent_win)
{
    m_capture_win = new MouseCaptureWindow(parent_win); // 创建隐藏的鼠标捕获窗口
    m_capture_win->Hide();
    m_capture_win->GetEventHandler()->Connect(wxEVT_MOTION,
        wxMouseEventHandler(WidgetSpy::OnMouseMotion), NULL, this);
    m_capture_win->CaptureMouse();
}

在Mouse Motion事件响应函数中高亮窗口、获取窗口类名并显示:

void OnMouseMotion()
{
    wxWindow *target_win = ::wxFindWindowAtPoint(::wxGetMousePosition()); // 查找窗口
    if (target_win == NULL) return;

    wxString class_name = GetClassNameOf(target_win); // 获取窗口类名

    if (target_win != m_last_window && m_last_window != NULL)
    {
        UpdateWindow(m_last_window);  // 刷新旧窗口
    }
    HighlightWindow(target_win);      // 在目标窗口上绘制高亮框

    ShowSpyTip(m_parent_window, class_name, this, this); // 使用tooltip显示类名
    m_last_window = target_win;
}

由于typename(object).name()获取的是唯一标识符,编译器会对其做一些修饰,gcc提供了相应的函数来获取真正的类名:

wxString GetClassNameOf(wxWindow *win)
{
    if (NULL == win)
    {
    	 return "";
    }
    int status;
    const char *name = abi::__cxa_demangle(typeid(*win).name(), 0, 0, &status);
    if (status != 0)
    {
	     return "";
    }
    return name;
}

3. 高亮窗口的刷新

高亮窗口不仅需要在未知任意窗口上绘图,还需要在鼠标移开时刷新原窗口。绘图操作可以使用wxScreenDC,但是刷新操作使用wxWidgets提供的wxWindow::Update(), wxWindow::Refresh()很多时候并不能工作,所以这里采用手动截取图像,刷新时贴图的方法解决。 在Spy开启时,保存整个Frame的截图:

wxBitmap CaptureWindow(wxWindow *win)
{
    wxRect rc = win->GetScreenRect();
    wxMemoryDC memDC;
    wxBitmap image = wxBitmap(rc.width, rc.height);
    memDC.SelectObject(image);
    wxScreenDC srcDC;
    memDC.Blit(0, 0, rc.width, rc.height, &srcDC, rc.GetLeft(), rc.GetTop());
    return image;
}

高亮窗口时:

void HighligthWindowInMemory(wxWindow *win)
{
   	    wxRect rc = win->GetScreenRect();
   	    wxScreenDC dc;
   	    dc.SetBrush(*wxTRANSPARENT_BRUSH);
   	    wxPen *pen = wxThePenList->FindOrCreatePen(*wxRED, 3, wxSOLID);
   	    dc.SetPen(*pen);
   	    rc.Deflate(2, 2);
   	    wxScreenDC.DrawRectangle(2, 2, rc.width, rc.height);
}

刷新窗口时:

void UpdateWindow(wxWindow *win)
{
   	    wxRect rc_win = win->GetScreenRect();
   	    wxRect rc_main = m_parent_window->GetScreenRect();
   	    wxScreenDC src_dc;
   	    wxMemoryDC mem_dc;
   	    mem_dc.SelectObject(m_image_main);
   	    src_dc.Blit(rc_win.x, rc_win.y, rc_win.width, rc_win.height,
   		 &mem_dc, rc_main.x - rc_main.x, rc_main.y - rc_main.y);
}

另外,为了避免窗口绘图时的闪烁,实现时采用了双缓冲。

结果

编译出动态连接库libspy.so,Spy窗口时只需要设置LD_PRELOAD指向该动态链接库,就可以在GUI程序中使用快捷键开启spy。 缺点:由于libspy.so和GUI应用程序都需要链接到wxWidgets,当GUI静态链接到wxWidgets时,libspy.so就不能正确链接。所以这种方法只能在GUI动态链接到wxWidgets时使用。

展望

目前只是简单实现了获取窗口的类名的功能,并没有实现Spy++中获取窗口树形结构、属性等功能。但是我们已经可以向GUI程序注入任意代码(包括wxWidgets调用),进一步的实现也是水到渠成的了。



-EOF-
睿初科技软件开发技术博客,转载请注明出处

blog comments powered by Disqus