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);
}
}

7 comments:

  1. Anonymous4:07 PM

    How can that work: I see two references to SetSysColorsTemp() in that code, and they don't even agree on argument order. Efforts to use that undocumented function in W98 will cause chaos that only a reboot can fix. Unlike the standard function SetSysColors(), no arder of args, or substituion of brushes with colorrefs, etc, will provoke a rational result. Please can you advise a MINIM correct call to this undocumented SetSysColorsTemp(), with the absolute minimum of vital context? that way we can figure out the rest ourselves. I'm assuming that unlike the standard SetSysColors(), which uses existing brushes, we cannot know what to delete having had to make our own, unless we DO make our own, hence passing brushes to SetSysColoursTemp() instead of colorrefs for it to make its own brushes... Apart from that, I have seen nothing that leads to rational thought because the results of experiments are so broken that the only rational response is: REBOOT AT ONCE.

    I think the long term rational response is to avoid this undocumented function as an absolute plague pit of undefined behaviour, but I'm still interested in see if it ever worked, because in theory it is the most elegant way to cause one window to have its own system colours.

    ReplyDelete
  2. As far as I can see above, all calls to SetSysColorsTemp are consistent above. The code above isn't meant to be a recommendation to use SetSysColorsTemp, it's merely something I did and wanted to write down. In the end the result had a few too many glitches (e.g. the area around the close button sometimes repainting in the wrong color) and I didn't make use of the code in anger. With the move to Win Vista+ and a different theming mechanism, I can't imagine this technique will ever be of much use.

    ReplyDelete
  3. Anonymous10:33 PM

    Some of us stay with W98 because we can do more with it, and we're adapted to it, not fighting a constant 'upgrade' path. :) Doesn't matter though, SetSysColorsTemp() was always doomed to fail. It's intended only to make the preview image of controls and windows in that control panel thinger, Display Properties.. I have a scheme thta makes black frames, just enough grey to simulate light on dark edges to indicate borders and such. Orange button text.. Very nice, but if I try to make it happen against the will of the system some truly crazy stuff happens! Black becomes white, in the MOST unexpected places, and I could set the LEFT of INactive title, and RIGHT of active, but NOT both at once for either state because if I did the whole title went black! It's such an astounding mess that the person who said in the Wine development stuff, that this function shouldn't even be there at all, was totally right. That 15 years passed with NO answer to this problem, let alone a definitive one, speaks with a roar like a rocket engine. OwnerDraw stuff seems liek too much hassle, until the 'simple' way has been hammered to death over the course of 24 hours or so. :)

    ReplyDelete
  4. Anonymous12:29 AM

    Small followup...
    CreateWindow(ClassName,PrgN,WS_POPUP+WS_SYSMENU,Px,Py,Sx,Sy,NULL,NULL,H_inst,NULL);

    That one is awesome, it has no client area so anything goes. It even responds to window close messages as usual, and shows a move/close menu on request. No need for OwnerDraw or any other complication other than a bit of message handling, and possibly subclassing static controls to the main WndProc. >:) The power and ease of that scheme, and its ability to completely specify all appearance details, using nothing that is not well-documented and easy to use, pretty much guarantees that while I code on W98, the results will likely port to other systems largely intact.

    It might be limiting to a person needing to resize a window easily, but I don't, so this is definitely the answer for me. I've already done things with small bitmaps and subclassed statics that mean I have all the methods I'll need for emulating the look and behaviour of any window or button with no 'OwnerDraw' complications, but using a window with no client area is ideal. I imagine someone at Microsoft wisely built an easy way to avoid their windows at will if we need to.

    ReplyDelete
  5. Anonymous2:02 PM

    Another thought, and a question...
    I've seen system colours persist in some controls, in various programs, after a change. Is there a way to locate some local palette and force the colours we want into that one, overriding the main system colours? It might not solve the repaint of areas in a title bar around buttons (or on the other hand, maybe it will, given that this local persistence is probably the cause of that issue anyway), but if nothing else it could be worth exploring.

    ReplyDelete
  6. Anon: No idea - I no longer have access to a system with gradient title bars, so for me this post is mostly capturing historic information.

    ReplyDelete