我们要学会查看技术细节点的文档化说明

目录

1、概述

2、GDI绘图遇到的问题

2.1、创建兼容bitmap应该使用哪个DC 

2.2、一个bitmap位图不能同时选进多个dc中

3、无从下手的GDI资源泄漏问题

4、总结


1、概述

       当我们在使用Windows API遇到问题时,要想到使用微软的MSDN(在线MSDN或者本地安装的MSDN),到MSDN上查看目标API的详细说明和注解(Remark部分),比如下图中的系统API函数GetWIndowText:

也许就能找到问题的解决办法。

        此外,在使用一些辅助软件工具遇到问题时,如果工具有官网的话,可以尝试到官网上查看一下该工具的详细说明,也可能找到问题的解决思路或突破口。

       本文提及技术细节点的文档化说明,是指微软MSDN或软件工具官网上给出的公开的说明文字和注解。本文结合解决的几个实际问题,来说明文档化说明的重要性。

2、GDI绘图遇到的问题

2.1、创建兼容bitmap应该使用哪个DC 

       在Windows GDI绘图中,有个双缓冲绘图的概念,即现在内存DC上绘图,待绘制成功后再将内存DC中的内容绘制到窗口上。

      在使用双缓冲绘图时,我们一般先创建一个与窗口DC兼容的内存DC,然后再创建一个兼容的bitmap,然后将bitmap选中内存DC中,bitmap是画板,将要绘制的内容先绘制到bitmap上。在创建兼容bitmap时传入的dc参数是有讲究的。如果传入的是刚创建的兼容dc,代码如下:

HDC hdc = ::GetDC( this->m_hWnd );  // 窗口DC
HDC hMemDC = ::CreateCompatibleDC( hdc );   // 兼容内存DC
HBITMAP hBitmap = ::CreateCompatibleBitmap( hMemDC, 800, 600 ); // 传入的是刚创建的兼容DC

感觉上是没问题的,hMemDC是与窗口的hdc是兼容的,想到可能存在的传递性,创建的bitmap应该也是与hdc兼容的,运行应该是没问题的。

       但实际运行后,绘制到窗口的图片都变成了单一的灰白色。这是怎么回事呢?到MSDN中查看CreateCompatibleBitmap API的函数说明,在Remarks部分找到了如下的说明:

所以,根据上述微软的说明,如果使用我们上面给出的代码,创建出来的兼容位图是单色的位图,绘制出来的效果是单一的灰白色。MSDN上明确指出,传入到CreateCompatibleBitmap中的dc应该和CreateCompatibleDC传入的一致,都使用窗口dc,代码如下:

HDC hdc = ::GetDC( this->m_hWnd );
HDC hMemDC = ::CreateCompatibleDC( hdc );
HBITMAP hBitmap = ::CreateCompatibleBitmap( hdc, 800, 600 ); // 传入的是刚创建的兼容DC,这样处理就没问题了。

2.2、一个bitmap位图不能同时选进多个dc中

       假设位图m_hBitmap是UI类的成员变量,并且已经将要绘制到窗口上的图片加载到该位图中了,在窗口的OnPaint函数中会使用到该位图进行窗口的绘制,相关的代码片如下所示:

HDC hdc = ::GetDC( this->m_hWnd );
HDC hMemDC = ::CreateCompatibleDC( hdc );
HBITMAP hOldBitmap = (HBITMAP)::SelectObject( hdc, m_hBitmap )
...... // 此处使用BitBlt或者其他的函数进行绘制,代码省略

调试代码时,因为急着看到界面效果(看看代码有没有生效),没有讲究代码的规范性,没有将创建的兼容dc(hMemDC)给delete掉。运行程序后发现,只有第一次绘制是有效的,窗口上显示时正常的,接下来的每次窗口刷新时的绘制都是无效的。

       绘制无效时,查看绘制函数BitBlt的返回值,是调用成功的返回值。这又是怎么回事呢?明明添加了代码,怎么没有起到应有的效果呢?事后,在SelectObject API函数的MSDN说明中,有下面的一句话:

即不能将一个位图同时选进多个dc中,看到这句话我们似乎知道出问题的原因了。

       在OnPaint函数中的绘制代码,在每次窗口刷新时都会执行OnPaint中,所以上述代码会被多次执行,因为创建的兼容dc(hMemDc)没有delete掉,下次再执行到上述代码片,又会创建新的兼容dc,接着将位图m_hBitmap选进兼容dc,这样就导致位图m_hBitmap选进了多个dc中了,所以出现了上述问题。

       这个问题也提醒我们,平时在写代码要注重规范性,不用的资源要及时释放掉(这也是新手在写代码容易犯得的错误,这个问题就是在帮新人排查问题代码时遇到的)。就上述代码片,正确的做法是将创建的兼容dc在用完后delete掉,这样就不会出现同一个位图同事被选进两个dc中的问题了,正确的代码如下:

HDC hdc = ::GetDC( this->m_hWnd );
HDC hMemDC = ::CreateCompatibleDC( hdc );
HBITMAP hOldBitmap = (HBITMAP)::SelectObject( hMemDC, m_hBitmap )

...... //此处使用BitBlt或者其他的函数进行绘制,代码省略

::SelectObject( hMemDC, hOldBitmap );
::DeleteDC( hMemDC ); // 将创建的兼容dc释放掉

3、无从下手的GDI资源泄漏问题

       经测试同事反馈,我们的软件在打开部分窗口后关闭,GDI句柄数在每次操作后都会上升6个,多次操作后GDI对象会有明显的较大的飙升现象。可以通过Windows资源管理器来实时监测目标进程的GDI个数:(在任务管理器的详细信息标签页中)

这说明软件中是有GDI资源泄漏的。如果有GDI对象泄漏,在程序长时间运行时,如果程序的GDI对象个数达到上万个,就会导致程序异常甚至崩溃。所以,这个问题必须要重视,是必须要解决的。

在默认情况下,打开Windows资源管理器,默认是看不到GDI对象这一列的,需要右键点击进程列表的标题栏,弹出如下的右键菜单:

然后点击“选择列”菜单项,在弹出的窗口中勾选“GDI对象”:

然后在进程列表中就可以看到GDI对象列了。

       在测试过程中,还发现部分窗口在打开关闭后是没有泄漏的。下面该GDI泄漏排查利器GDIView上场了:

通过使用GDI资源泄漏排查工具GDIView,当GDI资源泄漏时,观察到GDI Total列没有增长,All GDI列每次都有增长(上升6个)。一般我们的资源泄漏主要是pen、bitmap、font、brush、region、dc等常见的GDI对象,结果这次这些常规GDI对象一个都没有泄漏,这就比较奇怪了,感觉无从查起了。

       于是尝试着到GDIView的官网(http://www.nirsoft.net/utils/gdi_handles.html)上看一下,看看有没有相关的说明。结果找到了对应的说明,如下:

即如果出现其他GDI数量没有增长,只有All GDI列出现增长,有可能是创建的图标或光标资源没有释放引起的。
       于是排查了处理图标和光标资源的代码,但并没有找到可能存在泄漏的地方。无意中发现,当有的窗口在任务栏有图标时,就会有泄漏,如果窗口没有任务栏窗口(比如程序的关于窗口等),就不会有泄漏。那是不是设置任务栏窗口图标的代码有问题,于是找到对应的代码:

void CWindowWnd::SetIcon( UINT nRes )
{
    HICON hIcon = (HICON)::LoadImage( CPaintManagerUI::GetInstance()
                                , MAKEINTRESOURCE(nRes)
                                , IMAGE_ICON
                                , ::GetSystemMetrics(SM_CXICON)
                                , ::GetSystemMetrics(SM_CYICON)
                                , LR_DEFAULTCOLOR );
    ASSERT( hIcon );
    ::SendMessage( m_hWnd, WM_SETICON, (WPARAM)TRUE, (LPARAM)hIcon );

    hIcon = (HICON)::LoadImage( CPaintManagerUI::GetInstance()
                            , MAKEINTRESOURCE(nRes)
                            , IMAGE_ICON
                            , ::GetSystemMetrics(SM_CXSMICON)
                            , ::GetSystemMetrics(SM_CYSMICON)
                            , LR_DEFAULTCOLOR );
    ASSERT( hIcon );
    ::SendMessage( m_hWnd, WM_SETICON, (WPARAM)FALSE, (LPARAM)hIcon );
}

难道是LoadImage API函数使用的有问题?于是到MSDN上详细查看了该函数的说明,找到了出问题的地方:

即如果没有使用LR_SHARED标记位,则需要调用DestroyXXX接口去手动释放这些资源。
       上面的代码中没有使用这个标记位,也没有释放加载的图标资源,所以出现了图标资源的泄漏。解决办法是,在加载图标时,添加上LR_SHARED标记位。修改后的代码如下:

void CWindowWnd::SetIcon( UINT nRes )
{
    HICON hIcon = (HICON)::LoadImage( CPaintManagerUI::GetInstance()
                                , MAKEINTRESOURCE(nRes)
                                , IMAGE_ICON
                                , ::GetSystemMetrics(SM_CXICON)
                                , ::GetSystemMetrics(SM_CYICON)
                                , LR_DEFAULTCOLOR|LR_SHARED ); // 添加LR_SHARED标记位
    ASSERT( hIcon );
    ::SendMessage( m_hWnd, WM_SETICON, (WPARAM)TRUE, (LPARAM)hIcon );

    hIcon = (HICON)::LoadImage( CPaintManagerUI::GetInstance()
                            , MAKEINTRESOURCE(nRes)
                            , IMAGE_ICON
                            , ::GetSystemMetrics(SM_CXSMICON)
                            , ::GetSystemMetrics(SM_CYSMICON)
                            , LR_DEFAULTCOLOR|LR_SHARED );
    ASSERT( hIcon );
    ::SendMessage( m_hWnd, WM_SETICON, (WPARAM)FALSE, (LPARAM)hIcon );
}

       此外,为了更深入的理解问题,还可以看一下LR_SHARED标记位的说明:

从上面说明得知,如果使用了LR_SHARED标记位,则系统在图标资源不再使用时去负责销毁图标资源。

4、总结

       作为一个合格的Windows开发人员,要养成遇到问题就查MSDN的好习惯。MSDN上的文档化的详细说明和描述,可能就能帮助我们解决遇到的问题。对于一些工具,则可以尝试看看官网的一些注释和说明。

猜你喜欢

转载自blog.csdn.net/chenlycly/article/details/126004453