Visual Studio .NET 的壁纸






4.33/5 (26投票s)
2004年3月5日
5分钟阅读

225627

3195
如何为 Visual Studio .NET 编辑窗口添加背景图像。
- 下载源文件 - 35.9 Kb
- 下载安装程序 - 92 Kb
- 下载源文件 v1.1 (仅限 XP) - 35.9 Kb
- 下载安装程序 v1.1 (仅限 XP) - 92 Kb
- 下载源文件 v1.2 (仅限 XP) - 36.1 Kb
- 下载安装程序 v1.2 (仅限 XP) - 92.2 Kb
引言
这个项目是我为 ClearJump 做的一些研究的成果。ClearJump 慷慨地允许我发布源代码。
有关版本详情,请参阅文章末尾的历史记录部分。
在 Visual Studio .NET 中,您可以更改文本编辑器的背景颜色,但不能将其设置为背景图片。因此,我决定研究是否可以创建一个 Visual Studio 插件来支持壁纸位图。除了滚动时闪烁的问题,它实际上也起作用了,如下所示。
稍后我将详细讨论闪烁问题。如果您找到了解决方法,请告诉我。现在,言归正传,谈谈壁纸。
即使您不使用此插件将位图添加到 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.cpp 和 image.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()
。我还不确定如何处理这个问题。
结论
如果您有任何很酷的壁纸,请告诉我。