Introduction
This project is the result of some research that I did for ClearJump. ClearJump was kind
enough to give me permission to publish the source code.
For details about the versions, see the History section at the end of the
article.
In Visual Studio .NET, you can change the background color of the text editor
but you cannot put an image in it as a background wallpaper. So, I decided to
look at whether a Visual Studio Add-In could be made to provide support for a
wallpaper bitmap. Except for a problem with flashing during scrolling, it
actually worked as you can see below.
I'll discuss the flashing problem in detail later. If you find a solution to
fix it, please let me know. Now, on with the wallpaper.
Even if you don't use this Add-In for adding a bitmap to your Visual Studio
interface, the project may still be of interest for its use of a little known
window subclassing technique. The project also features some image processing
classes. For example, the Image
class supports pixel-level access
to the bitmap and can be used for adding alpha channels.
This project was created using the .NET Add-In wizard and has been tested on
Visual Studio version 7.1.
Installation
- Run the the VSWallpaperSetup.msi file.
- Restart Visual Studio.
- The Wallpaper menu should now appear in the Tools menu.
Enjoy!
Implementation
I was not able to find any .NET automation interfaces that support custom
backgrounds. The best you can do programmatically is change the background color
properties which is the same limited control that you have from the
Tools/Options menu command. So I decided to subclass the text editor window and
put my background code into the WM_ERASEBKGND
handler.
Unfortunately, .NET automation doesn't expose the window handles
(HWND
) of text editor windows. However, it is possible to get the
main window handle.
HWND hwnd;
CComPtr<EnvDTE::Window> main;
m_pDTE->get_MainWindow((EnvDTE::Window**)&main) );
main->get_HWnd((long*)&hwnd) );
After some poking around, I retrieved the undocumented .NET window class
names. Fortunately, .NET automation generates events when an editor text window
is created or destroyed. The wallpaper Add-In processes these events. Using the
window caption that is reported by these events and the window class names, the
Add-In finds the appropriate window handle. You can see this by looking at the
code for the EditorInstance
class. This class uses the main window
handle, editor window caption and editor window class name to find the editor
window handle. The process is very straightforward and consists of calling the
FindWindowEx()
and EnumChildWindows()
Win32
functions.
Next, I tried to use the SetWindowLong()
API to subclass the
window but that didn't work. The SetWindowLong()
function should
return a pointer to the previous window procedure in the call chain. Well in
this case, it returned some number that is not a valid pointer.
It turns out that .NET uses the little known SetWindowSubclass()
API. This API is supported by the comctl32.dll version 5.8 or later. So,
you need to include the commctrl.h header and add comctl32.lib to
your project. After that, the subclassing code is easy.
#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 )
{
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 )
{
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
)
{
SubclassedWindow *pw = (SubclassedWindow *)dwRefData;
if( pw ) return pw->winproc(uMsg, wParam, lParam);
return 0;
}
Now we can process the WM_ERASEBKGND
message. But wait. Not so
fast! Changing the background in the message handler actually doesn't work.
Believe it or not, whatever you paint in the WM_ERASEBKGND
handler
will be repainted by VS.NET. Interestingly, VS.NET paints the background in the
WM_PAINT
message along with the text. So, I came up with the
following workaround.
First, prepare the background image.
- Read the image file (BMP, GIF or JPEG).
- Alpha-blend the image file with the background color that is specified by
the user in Tools/Options.
Now we have a ready-to-use background image.
In the WM_PAINT
message handler, we do the following:
- Call the .NET painting routine.
- From the editor window, extract and save the new image.
BitBlt
our custom image.
TransparentBlt
the saved image. The transparent color is the
user's selected background color. This step paints the text on top of the
custom background image that we painted in the previous step.
To give you an idea of what this looks like, below is the code that does the
painting.
LRESULT EditorWindow::winproc( UINT uMsg, WPARAM wParam, LPARAM lParam )
{
if( uMsg == WM_PAINT )
{
RECT rc;
GetUpdateRect( m_hwnd, &rc, FALSE );
HDC hdc;
hdc = ::GetDC( m_hwnd );
HBITMAP img = CreateCompatibleBitmap( hdc,
rc.right - rc.left, rc.bottom - rc.top );
HDC hmem = ::CreateCompatibleDC( hdc );
HBITMAP hold = (HBITMAP)::SelectObject(hmem, (HGDIOBJ)img);
LRESULT lr = 0;
lr = SubclassedWindow::winproc( uMsg, wParam, lParam );
if( !::IsRectEmpty(&rc) )
{
::HideCaret( m_hwnd );
BitBlt(hmem, 0,0,
rc.right - rc.left, rc.bottom - rc.top,
hdc,
rc.left, rc.top,
SRCCOPY);
m_connect->m_background.draw( hdc,
m_connect->m_bkg_color,
m_connect->m_config.m_transparency, rc );
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 );
::ShowCaret( m_hwnd );
}
::SelectObject(hmem, hold );
::DeleteObject( (HGDIOBJ)img );
::DeleteDC(hmem);
::ReleaseDC( m_hwnd, hdc );
return lr;
}
return SubclassedWindow::winproc( uMsg, wParam, lParam );
}
You can see from the code why the edit window flashes during scrolling,
especially when the update region is large. The flashing occurs when the .NET
painting routine is called and then we repaint it with our background image.
Ugly. Again, if you figure out how to fix or work around this problem, please
let me know.
For alpha-blending, we use the ImageLib::Image
class that's
found in the image.cpp and image.h files. This class is useful for
accessing bitmap data directly. With it, you can initialize an
Image
object with your bitmap and then access individual pixels.
The createAlphaBitmap()
function can be used for adding an alpha
channel to the image. This function returns HBITMAP
that can be
used directly in the AlphaBlend()
function.
class Image
{
public:
Image();
virtual ~Image();
Result allocate(size_t rows, size_t cols);
void destroy(void);
Result load(HBITMAP hnd);
HBITMAP createBitmap(HDC hdc) const;
HBITMAP createAlphaBitmap(HDC hdc,
COLORREF c, AlphaComponent alpha ) const;
Result createDIB( Dib& dib ) const;
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; }
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;
}
};
History
v1.1 (XP only)
I think that we are one step closer to solving the flashing problem on XP at
least. It's almost gone now. Could someone please test it on Win2k? Thanks!
The key is in using the little know function, PrintWindow()
.
This function allows you to send a memory device context to the
WM_PAINT
handler. So that BeginPaint()
will get the
memory DC instead of the normal window DC.
The painting code look like this now.
LRESULT EditorWindow::winproc( UINT uMsg, WPARAM wParam, LPARAM lParam )
{
if( uMsg == WM_PAINT && m_connect && m_connect->m_config.m_enabled
&& !m_reentry )
{
RECT rc, cr;
::GetUpdateRect( m_hwnd, &rc, FALSE );
if( !::IsRectEmpty(&rc) )
{
::GetClientRect( m_hwnd, &cr );
HDC hdc;
hdc = ::GetDC( m_hwnd );
HBITMAP img = CreateCompatibleBitmap( hdc, cr.right - cr.left,
cr.bottom - cr.top );
HDC hmem = ::CreateCompatibleDC( hdc );
HBITMAP hold = (HBITMAP)::SelectObject(hmem, (HGDIOBJ)img);
::HideCaret( m_hwnd );
m_reentry = true;
::PrintWindow( m_hwnd, hmem, PW_CLIENTONLY );
m_reentry = false;
m_connect->m_background.draw( hdc, m_connect->m_bkg_color,
m_connect->m_config.m_transparency,
rc );
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 );
::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 only)
Fixed visibility problems when the background doesn't cover all the text window.
The scrolling with scrollbar should be perfect now.
When scrolling with keyboard, the background still jumps up and down.
This is caused by an internal call to ScrollWindow()
or ScrollDC()
in the keystrokes message handler. I am not sure how to deal with that yet.
Conclusion
Let me know if you have any cool wallpapers.