65.9K
CodeProject 正在变化。 阅读更多。
Home

Visual Studio .NET 的壁纸

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.33/5 (26投票s)

2004年3月5日

5分钟阅读

viewsIcon

225627

downloadIcon

3195

如何为 Visual Studio .NET 编辑窗口添加背景图像。

引言

这个项目是我为 ClearJump 做的一些研究的成果。ClearJump 慷慨地允许我发布源代码。

有关版本详情,请参阅文章末尾的历史记录部分。

在 Visual Studio .NET 中,您可以更改文本编辑器的背景颜色,但不能将其设置为背景图片。因此,我决定研究是否可以创建一个 Visual Studio 插件来支持壁纸位图。除了滚动时闪烁的问题,它实际上也起作用了,如下所示。

Sample Image - VSWallpaper.png

稍后我将详细讨论闪烁问题。如果您找到了解决方法,请告诉我。现在,言归正传,谈谈壁纸。

即使您不使用此插件将位图添加到 Visual Studio 界面,该项目可能仍然有趣,因为它使用了一种鲜为人知的窗口子类化技术。该项目还包含一些图像处理类。例如,Image 类支持对位图进行像素级访问,并可用于添加 Alpha 通道。

此项目是使用 .NET Add-In 向导创建的,并在 Visual Studio 7.1 版本上进行了测试。

安装

  • 运行 VSWallpaperSetup.msi 文件。
  • 重新启动 Visual Studio。
  • 现在,“壁纸”菜单应该出现在“工具”菜单下。

尽情享用!

实现

我没有找到任何支持自定义背景的 .NET 自动化接口。通过编程,您最多只能更改背景颜色属性,这与您从“工具/选项”菜单命令获得的有限控制相同。因此,我决定子类化文本编辑器窗口,并将我的背景代码放入 WM_ERASEBKGND 处理程序中。不幸的是,.NET 自动化不公开文本编辑器窗口的窗口句柄 (HWND)。但是,可以获取主窗口句柄。

HWND hwnd;
CComPtr<EnvDTE::Window> main;
m_pDTE->get_MainWindow((EnvDTE::Window**)&main) );
main->get_HWnd((long*)&hwnd) );

经过一番探索,我检索到了未文档化的 .NET 窗口类名。幸运的是,.NET 自动化会在编辑器文本窗口创建或销毁时生成事件。壁纸插件会处理这些事件。使用这些事件报告的窗口标题和窗口类名,插件可以找到适当的窗口句柄。您可以通过查看 EditorInstance 类的代码来了解这一点。此类使用主窗口句柄、编辑器窗口标题和编辑器窗口类名来查找编辑器窗口句柄。该过程非常简单,包括调用 FindWindowEx()EnumChildWindows() Win32 函数。

接下来,我尝试使用 SetWindowLong() API 来子类化窗口,但没有成功。SetWindowLong() 函数应该在调用链中返回前一个窗口过程的指针。在这种情况下,它返回了一个无效的指针。

事实证明,.NET 使用了一个鲜为人知的 SetWindowSubclass() API。此 API 受 comctl32.dll 版本 5.8 或更高版本支持。因此,您需要包含 commctrl.h 头文件,并将 comctl32.lib 添加到您的项目中。之后,子类化代码就很容易了。

#include "StdAfx.h"
#include <commctrl.h>
#include ".\subclassedwindow.h"

#define SUBCLASS_ID (0xab01265)

LRESULT CALLBACK g_subclass( HWND hWnd,
    UINT uMsg,
    WPARAM wParam,
    LPARAM lParam,
    UINT_PTR uIdSubclass,
    DWORD_PTR dwRefData
);


SubclassedWindow::SubclassedWindow(HWND h) : m_hwnd(h)
{
    if( !m_hwnd ) return;
    ::SetWindowSubclass( m_hwnd, g_subclass, SUBCLASS_ID, (DWORD_PTR)this );
}

SubclassedWindow::~SubclassedWindow(void)
{
    if( !m_hwnd ) return;
    ::RemoveWindowSubclass( m_hwnd, g_subclass, SUBCLASS_ID );
    m_hwnd = NULL;
}


LRESULT SubclassedWindow::winproc( UINT uMsg, WPARAM wParam, LPARAM lParam )
{
    //WM_NCDESTROY is the last message, 
    //detach the object from the window handle
    if( uMsg == WM_NCDESTROY )
    {
        HWND hsave = m_hwnd;
        ::RemoveWindowSubclass( m_hwnd, g_subclass, SUBCLASS_ID );
        m_hwnd = NULL;
        return ::DefSubclassProc( hsave, uMsg, wParam, lParam );
    }

    return dispatch( uMsg, wParam, lParam );
}

LRESULT SubclassedWindow::dispatch( UINT uMsg, WPARAM wParam, LPARAM lParam )
{
    //call the next window proc in the chain
    return ::DefSubclassProc( m_hwnd, uMsg, wParam, lParam );
}

LRESULT CALLBACK g_subclass( HWND hWnd,
    UINT uMsg,
    WPARAM wParam,
    LPARAM lParam,
    UINT_PTR uIdSubclass,
    DWORD_PTR dwRefData
)
{
    //call the SubclassedWindow object that is attached to
    //this window
    SubclassedWindow *pw = (SubclassedWindow *)dwRefData;
    if( pw ) return pw->winproc(uMsg, wParam, lParam);
    return 0;
}

现在我们可以处理 WM_ERASEBKGND 消息了。但等等。没那么快!在消息处理程序中更改背景实际上不起作用。无论您在 WM_ERASEBKGND 处理程序中绘制什么,VS.NET 都会重新绘制它。有趣的是,VS.NET 在 WM_PAINT 消息中绘制背景以及文本。所以,我找到了以下解决方法。

首先,准备背景图像。

  • 读取图像文件(BMP、GIF 或 JPEG)。
  • 将图像文件与用户在“工具/选项”中指定的背景颜色进行 Alpha 混合。

现在我们有了一个可以直接使用的背景图像。

WM_PAINT 消息处理程序中,我们执行以下操作:

  • 调用 .NET 绘图例程。
  • 从编辑器窗口提取并保存新图像。
  • 使用 BitBlt 绘制我们的自定义图像。
  • 使用 TransparentBlt 绘制保存的图像。透明颜色是用户选择的背景颜色。此步骤会将文本绘制在我们在上一步中绘制的自定义背景图像之上。

为了让您了解这看起来是什么样子,下面是执行绘图的代码。

LRESULT EditorWindow::winproc( UINT uMsg, WPARAM wParam, LPARAM lParam )
{
    if( uMsg == WM_PAINT  )
    {
        //save the update rectangle
        RECT rc;
        GetUpdateRect( m_hwnd, &rc, FALSE );

        HDC hdc;
        hdc = ::GetDC( m_hwnd );

        //preapare an empty bitmap
        HBITMAP img = CreateCompatibleBitmap( hdc, 
                  rc.right - rc.left, rc.bottom - rc.top );
        HDC hmem = ::CreateCompatibleDC( hdc );
        HBITMAP hold = (HBITMAP)::SelectObject(hmem, (HGDIOBJ)img);

        //call the .NET handler
        //we now have the updated editor window
        LRESULT lr = 0;
        lr = SubclassedWindow::winproc( uMsg, wParam, lParam );

        //the update rectangle is not empty
        if( !::IsRectEmpty(&rc)  )
        {
            //hide the caret temporarily
            ::HideCaret( m_hwnd );

            //extract the image from the editor
            BitBlt(hmem, 0,0,
                    rc.right - rc.left, rc.bottom - rc.top,
                    hdc,
                    rc.left, rc.top,
                    SRCCOPY);


            //draw our background image
            m_connect->m_background.draw( hdc, 
                   m_connect->m_bkg_color, 
                   m_connect->m_config.m_transparency, rc );

            //blt the saved editor image with transparent background color
            TransparentBlt( hdc, 
                   rc.left, 
                   rc.top, 
                   rc.right - rc.left, 
                   rc.bottom - rc.top, 
                   hmem, 0, 0, 
                   rc.right - rc.left, 
                   rc.bottom - rc.top, 
                   m_connect->m_bkg_color );

            //restore caret
            ::ShowCaret( m_hwnd );
        }


        //clean up
        ::SelectObject(hmem, hold );
        ::DeleteObject( (HGDIOBJ)img );
        ::DeleteDC(hmem);

        ::ReleaseDC( m_hwnd, hdc );

        return lr;
    }
    return SubclassedWindow::winproc( uMsg, wParam, lParam );
}

从代码中可以看到,为什么在滚动时,尤其是在更新区域很大时,编辑窗口会闪烁。闪烁发生在调用 .NET 绘图例程,然后我们用背景图像重新绘制它时。很难看。同样,如果您找到了解决或绕过此问题的方法,请告诉我。

对于 Alpha 混合,我们使用 image.cppimage.h 文件中的 ImageLib::Image 类。此类可用于直接访问位图数据。使用它,您可以初始化一个 Image 对象以及您的位图,然后访问单个像素。createAlphaBitmap() 函数可用于向图像添加 Alpha 通道。此函数返回的 HBITMAP 可直接用于 AlphaBlend() 函数。

//Image class
//===========
class Image
{
public:
    Image();
    virtual ~Image();

    // allocate space for an image of the specified size
    Result allocate(size_t rows, size_t cols);
    // destroy the image
    void destroy(void);

    // initializes the Image object with the data from the bitmap
    Result load(HBITMAP hnd);

    // create bitmap from the image data
    HBITMAP createBitmap(HDC hdc) const;
    //add the alpha channel to the 'c' color and create bitmap
    //that can be used with AlphaBlend()
    HBITMAP createAlphaBitmap(HDC hdc, 
           COLORREF c, AlphaComponent alpha ) const;
    //create DIB from the image data
    Result  createDIB( Dib& dib ) const;  //creates 24bits DIB

    //pixel-level access
    inline Pixel*        getData() { return m_data; }
    inline const Pixel*  getData() const { return m_data; }
    inline size_t        getRows() const { return m_rows; }
    inline size_t        getCols() const { return m_cols; }
    // set the pixel color
    inline void setPixel(size_t row, size_t col, COLORREF cr);
    inline void setPixel( size_t row, size_t col,
                        PixelComponent r,
                        PixelComponent g,
                        PixelComponent b );
     inline Image& operator=( const Image& in )
     {
         if( this == &in ) return *this;
         if( getRows() != in.getRows() || getCols() != in.getCols() )
         {
             Result rc = allocate( in.getRows(), in.getCols() );
             assert( rc == rc_ok );
         }
         memcpy( m_data, in.m_data, m_rows*m_cols*sizeof(Pixel) );
         return *this;
     }
};

历史

v1.1 (仅限 XP)

我认为我们离解决 XP 上的闪烁问题又近了一步。现在几乎消失了。有人能请在 Win2k 上测试一下吗?谢谢!

关键在于使用鲜为人知的 PrintWindow() 函数。此函数允许您将内存设备上下文发送到 WM_PAINT 处理程序。这样 BeginPaint() 就会获得内存 DC 而不是普通的窗口 DC。

现在绘图代码看起来是这样的。

LRESULT EditorWindow::winproc( UINT uMsg, WPARAM wParam, LPARAM lParam )
{
    if( uMsg == WM_PAINT && m_connect && m_connect->m_config.m_enabled
        && !m_reentry )
    {
        //save the update rectangle
        RECT rc, cr;
        ::GetUpdateRect( m_hwnd, &rc, FALSE );
        
        //the update rectangle is not empty
        if( !::IsRectEmpty(&rc)  )
        {
            ::GetClientRect( m_hwnd, &cr );
        
            HDC hdc;
            hdc = ::GetDC( m_hwnd );
            
            //preapare an empty bitmap
            HBITMAP img = CreateCompatibleBitmap( hdc, cr.right - cr.left, 
                                                  cr.bottom - cr.top );
            HDC hmem = ::CreateCompatibleDC( hdc );
            HBITMAP hold = (HBITMAP)::SelectObject(hmem, (HGDIOBJ)img);
            
            //hide the caret temporarily
            ::HideCaret( m_hwnd );
            
            //call the .NET handler
            //we now have the updated editor window
            m_reentry = true;
            ::PrintWindow( m_hwnd, hmem, PW_CLIENTONLY );
            m_reentry = false;
                
            //draw our background image             
            m_connect->m_background.draw( hdc, m_connect->m_bkg_color, 
                                          m_connect->m_config.m_transparency,
                                          rc );
            
            //blt the saved editor image with transparent background color
            TransparentBlt( hdc, rc.left, rc.top, rc.right - rc.left, 
                            rc.bottom - rc.top, hmem, rc.left, rc.top, 
                            rc.right - rc.left, rc.bottom - rc.top, 
                            m_connect->m_bkg_color );

            //clean up  and validate        
            ::SelectObject(hmem, hold );
            ::DeleteObject( (HGDIOBJ)img );
            ::DeleteDC(hmem);
            
            ::ReleaseDC( m_hwnd, hdc ); 
            
            ::ValidateRect( m_hwnd, &rc );  
        
            ::ShowCaret( m_hwnd );
            return 0;
        }
    }
    
    return SubclassedWindow::winproc( uMsg, wParam, lParam );
}

v1.2 (仅限 XP)

修复了背景未覆盖所有文本窗口时的可见性问题。

现在滚动条滚动应该完美了。

使用键盘滚动时,背景仍然上下跳动。这是由于在按键消息处理程序中内部调用了 ScrollWindow()ScrollDC()。我还不确定如何处理这个问题。

结论

如果您有任何很酷的壁纸,请告诉我。

© . All rights reserved.