Saturday, June 18, 2011

Changing the Windows titlebar gradient colors

Summary: This post describes how to change the gradient colors of the titlebar of a single window on Windows XP, using C#.

On Windows XP the titlebar of most windows have a gradient coloring:



For various reasons, I wanted to override the titlebar gradient colors on a single window. The SetSysColors function can change the colors, but that applies to all windows and immediately forces all windows to repaint, which is slow and very flickery. You can paint the titlebar your self (like Chrome does), but then you need to draw and handle min/max buttons etc. After some experimentation, using the unofficial SetSysColorsTemp function, I was able to change the gradient for a single window. As an example, here are some screenshots, overriding the left color with green, then overriding both the colors with green/blue and orange/blue:



Disclaimer: This code uses an undocumented function, and while it seems to work robustly on my copy of Windows XP, it unlikely to work on future versions of Windows. Generally, it is a bad idea to override user preferences - for example the titlebar text may not be visible with overridden gradient colors.

All the necessary code is available at the bottom of this post, and can be used by anyone, for any purpose. The crucial function is SetSysColorsTemp, which takes an array of colors and an array of brushes created from those colors. If you call SetSysColorsTemp passing a new array of colors/brushes they will override the global system colors until a reboot, without forcing a repaint. Passing null for both arrays will restore the colors to how they were before. To make the color change only for the current window we hook into WndProc, and detect when the colors are about to be used in a paint operation. We set the system colors before, call base.WndProc which paints the titlebar (using our modified system colors), then put the colors back.

There are two known issues:

1) If you change the colors after the titlebar has drawn, it will not use the colors until the titlebar next repaints. I suspect this problem is solvable.
2) As you can see in the demo screenshots, the area near the min/max buttons does not usually get recolored. If you only change the left gradient color this doesn't matter.



public class GradientForm : Form
{
// Win32 API calls, often based on those from pinvoke.net
[DllImport("gdi32.dll")] static extern bool DeleteObject(int hObject);
[DllImport("user32.dll")] static extern int SetSysColorsTemp(int[] lpaElements, int [] lpaRgbValues, int cElements);
[DllImport("gdi32.dll")] static extern int CreateSolidBrush(int crColor);
[DllImport("user32.dll")] static extern int GetSysColorBrush(int nIndex);
[DllImport("user32.dll")] static extern int GetSysColor(int nIndex);
[DllImport("user32.dll")] private static extern IntPtr GetForegroundWindow();

// Magic constants
const int SlotLeft = 2;
const int SlotRight = 27;
const int SlotCount = 28; // Math.Max(SlotLeft, SlotRight) + 1;

// The colors/brushes to use
int[] Colors = new int[SlotCount];
int[] Brushes = new int[SlotCount];

// The colors the user wants to use
Color titleBarLeft, titleBarRight;
public Color TitleBarLeft{get{return titleBarLeft;} set{titleBarLeft=value; UpdateBrush(SlotLeft, value);}}
public Color TitleBarRight{get{return titleBarRight;} set{titleBarRight=value; UpdateBrush(SlotRight, value);}}

void CreateBrushes()
{
for (int i = 0; i < SlotCount; i++)
{
Colors[i] = GetSysColor(i);
Brushes[i] = GetSysColorBrush(i);
}
titleBarLeft = ColorTranslator.FromWin32(Colors[SlotLeft]);
titleBarRight = ColorTranslator.FromWin32(Colors[SlotRight]);
}

void UpdateBrush(int Slot, Color c)
{
DeleteObject(Brushes[Slot]);
Colors[Slot] = ColorTranslator.ToWin32(c);
Brushes[Slot] = CreateSolidBrush(Colors[Slot]);
}

void DestroyBrushes()
{
DeleteObject(Brushes[SlotLeft]);
DeleteObject(Brushes[SlotRight]);
}

// Hook up to the Window

public GradientForm()
{
CreateBrushes();
}

protected override void Dispose(bool disposing)
{
if (disposing) DestroyBrushes();
base.Dispose(disposing);
}

protected override void WndProc(ref System.Windows.Forms.Message m)
{
const int WM_NCPAINT = 0x85;
const int WM_NCACTIVATE = 0x86;

if ((m.Msg == WM_NCACTIVATE && m.WParam.ToInt32() != 0) ||
(m.Msg == WM_NCPAINT && GetForegroundWindow() == this.Handle))
{

int k = SetSysColorsTemp(Colors, Brushes, Colors.Length);
base.WndProc(ref m);
SetSysColorsTemp(null, null, k);
}
else
base.WndProc(ref m);
}
}

1 comment:

Anonymous said...

Custom Title Bar