Introduction
This article intends to show you how to change the color of a Windows CE device's components in a simple way. What would you do if, for example, you want to display a red background for your button? Most likely, you will have to catch the WM_PAINT
message and do all the work by yourself. And what about changing the text color displayed in the title bar? You'll have to redraw the non-client area.
Furthermore, let's say that your customer wants you to create themes in your system. That is, for the theme X, I want a pink background for the windows, a red background for the buttons, white color for the static text, and a light blue background for the menus. And that is not all: the next month, the customer will want another theme.
In fact, the last paragraph described my situation not so long ago. What to do? Rewriting the application, catching the WM_PAINT
, and doing the redraw for each control was not an option. Fortunately, after some research, I found a solution. And after finding it, I decided to create an application that allows me to change the system's color easily.
The purpose of this article is, thus, to show how to change the system's colors, and also to show how this application works and how to use it.
Background
Windows CE has many configurable stuff. The one that we are interested in, the system colors, is located in the registry, in the following path:
HKEY_LOCAL_MACHINE\System\GWE\SysColor
This registry key is a 116-byte binary buffer. Each system color uses four bytes: one byte for red color, one for green, and one for blue; the fourth is always 0, although it is believed that for further versions of Windows CE, this byte will be used as the alpha color.
Therefore, there are twenty nine system components whose color can be changed (116/4 = 29). The following is a list of those components.
Registry Order |
System color |
Description |
0 |
COLOR_SCROLLBAR |
Color of the gray area of a scroll bar. |
1 |
COLOR_BACKGROUND |
Background color of the desktop window. |
2 |
COLOR_ACTIVECAPTION |
Color of the title bar of an active window. |
3 |
COLOR_INACTIVECAPTION |
Color of the title bar of an inactive window. |
4 |
COLOR_MENU |
Background color of a menu. |
5 |
COLOR_WINDOW |
Background color of a window. |
6 |
COLOR_WINDOWFRAME |
Color of a window frame. |
7 |
COLOR_MENUTEXT |
Color of the text in a menu. |
8 |
COLOR_WINDOWTEXT |
Color of the text in a window. |
9 |
COLOR_CAPTIONTEXT |
Color of the text in a title bar and of the size box and scroll bar arrow box. |
10 |
COLOR_ACTIVEBORDER |
Color of the border of an active window. |
11 |
COLOR_INACTIVEBORDER |
Color of the border of an inactive window. |
12 |
COLOR_APPWORKSPACE |
Background color of multiple document interface (MDI) applications. |
13 |
COLOR_HIGHLIGHT |
Color of an item selected in a control. |
14 |
COLOR_HIGHLIGHTTEXT |
Color of the text of an item selected in a control. |
15 |
COLOR_BTNFACE |
Color of the face of a button. |
16 |
COLOR_BTNSHADOW |
Shadow color of buttons for edges that face away from the light source. |
17 |
COLOR_GRAYTEXT |
Color of shaded text. This color is set to 0 if the current display driver does not support a solid gray color. |
18 |
COLOR_BTNTEXT |
Color of the text for push buttons. |
19 |
COLOR_INACTIVECAPTIONTEXT |
Color of the text in the title bar of an inactive window. |
20 |
COLOR_BTNHIGHLIGHT |
Highlight color of buttons for edges that face the light source. |
21 |
COLOR_3DDKSHADOW |
Color of the dark shadow for three-dimensional display elements. |
22 |
COLOR_3DLIGHT |
Highlight color of three-dimensional display elements for edges that face the light source. |
23 |
COLOR_INFOTEXT |
Color of the text for ToolTip controls. |
24 |
COLOR_INFOBK |
Background color for ToolTip controls. |
25 |
COLOR_STATIC |
Background color for static controls and dialog boxes. Supported in Windows CE 2.0 and later. |
26 |
COLOR_STATICTEXT |
Color of the text for static controls. Supported in Windows CE 2.0 and later. |
27 |
COLOR_GRADIENTACTIVECAPTION |
Color of the title bar of an active window that is filled with a color gradient. |
28 |
COLOR_GRADIENTINACTIVECAPTION |
Color of the title bar of an inactive window that is filled with a color gradient. |
So, for locating the bytes you have to change, simple multiply the registry order times four. For example, for modifying the background color of a menu to red, you'll have to change the 16th byte to FF, the 17th to 00, and the 18th to 00 as well. Update the registry and perform a soft reset. Note: A soft reset (also called a warm boot) is needed, since these properties are read only when the system boots.
As you can see, the principle is somewhat simple. So, what a program must do is simply take the buffer, modify it, update the registry, and soft reset the device. Or you can do, as I did, modify the registry and create a CAB file that updates the registry. Either way, you need a way to modify the registry, so I made a little application that runs on the mobile device and updates the registry.
PPC Color Configurator
This is the application I made; you will find the source code at the beginning of this article. I tested it under Windows CE .NET (4.2), for Symbol MC50, an iPaq, and an Intermec 730 machines.
The application is dialog-based. Here is the declaration of the class for the main dialog:
class CPPCColorConfigDlg : public CDialog
{
public:
CPPCColorConfigDlg(CWnd* pParent = NULL);
enum { IDD = IDD_PPCCOLORCONFIG_DIALOG };
protected:
HICON m_hIcon;
virtual void DoDataExchange(CDataExchange* pDX);
virtual BOOL OnInitDialog();
virtual void OnSelectOption();
virtual void OnColorChange();
virtual void OnPaint();
virtual void OnUpdateSettings();
private:
void FillColorOptions();
void InitVectors();
void DisplayDescription();
void UpdateRegistry();
void ResetRegistry();
void ParseItemColors(int* pRed, int* pGreen, int* pBlue);
void SetColorsInBoxes(int iRed, int iGreen, int iBlue);
int m_iSelected;
int m_iRed;
int m_iGreen;
int m_iBlue;
BYTE m_pNewBuffer<BUFFER_SIZE>;
BYTE m_pOldBuffer<BUFFER_SIZE>;
DECLARE_MESSAGE_MAP()
};
The member variable m_iSelected
holds the index of the selected attribute (i.e., menu background color) and is changed within the OnSelectOption
method. The properties m_iRed
, m_iGreen
, and m_iBlue
hold the values for the red, green, and blue textboxes, and whose value will determine the color of the component. Finally, the properties m_pNewBuffer
and m_pOldBuffer
hold the buffer of memory that will be taken from the registry and whose value will be eventually updated to the registry as well. The macro BUFFER_SIZE
is defined as 116.
The method InitVectors
initializes the memory buffers, taking the values from the registry. Here is the code:
void CPPCColorConfigDlg::InitVectors()
{
HKEY hKey;
DWORD dwType;
DWORD dwSize;
dwType = 0;
dwSize = BUFFER_SIZE;
memset(m_pNewBuffer, 0, BUFFER_SIZE);
memset(m_pOldBuffer, 0, BUFFER_SIZE);
::RegOpenKeyEx(HKEY_LOCAL_MACHINE, _T("SYSTEM\\GWE"), NULL, NULL, &hKey);
::RegQueryValueEx(hKey, _T("SysColor"), NULL, &dwType, m_pOldBuffer, &dwSize);
::RegQueryValueEx(hKey, _T("SysColor"), NULL, &dwType, m_pNewBuffer, &dwSize);
::RegCloseKey(hKey);
ASSERT(!memcmp(m_pOldBuffer, m_pNewBuffer, BUFFER_SIZE));
}
I created a global variable, g_vtrShowStrings
, which is a 2D matrix that holds the strings displayed in the combo box, and its description. The position within the matrix will determine the position within the buffer whose bytes will be modified. The macro OPTION_VECTOR_SIZE
is defined as 29.
CString g_vtrShowStrings[OPTION_VECTOR_SIZE][2] =
{
{ CString("Scrollbar"), CString("Color of the gray area of a scroll bar") },
{ CString("Background"), CString("Background color of the desktop window") },
{ CString("Active Caption"), CString("Color of the title bar of an active window") },
...
};
Just for the records, I present the message map. As you noticed from the picture, there are some controls: a combo box that holds the components of the system that can be updated, four text boxes whose purpose is to allow the user to change the color of the component (the first one is for red, the second is for green, and the third is for the blue color; the fourth is unused, and it will be for the alpha, when WinCE admits that parameter). There is also a button, that -when clicked- will update the registry through the memory buffer. Here is the message map:
BEGIN_MESSAGE_MAP(CPPCColorConfigDlg, CDialog)
ON_WM_PAINT()
ON_CBN_SELCHANGE(IDC_CMB_COMPONENTS, OnSelectOption)
ON_EN_CHANGE(IDC_TXT_RED, OnColorChange)
ON_EN_CHANGE(IDC_TXT_GREEN, OnColorChange)
ON_EN_CHANGE(IDC_TXT_BLUE, OnColorChange)
ON_BN_CLICKED(IDC_CMD_UPDATE, OnUpdateSettings)
END_MESSAGE_MAP()
The next thing is the OnInitDialog
. In such a dialog, we do two tasks: initialize the combo box according to the g_vtrShowStrings
matrix (the combo box's property "sorted" was unmarked in the dialog resource editor), and select the first item of the combo box. The method looks as follows:
BOOL CPPCColorConfigDlg::OnInitDialog()
{
CDialog::OnInitDialog();
SetIcon(m_hIcon, TRUE);
SetIcon(m_hIcon, FALSE);
CenterWindow(GetDesktopWindow());
FillColorOptions();
SetDlgItemText(IDC_TXT_OTHER, _T("0"));
SetDlgItemText(IDC_TXT_RED, _T("0"));
SetDlgItemText(IDC_TXT_GREEN, _T("0"));
SetDlgItemText(IDC_TXT_BLUE, _T("0"));
OnColorChange();
CComboBox* pColorOptions;
pColorOptions = reinterpret_cast<CComboBox*>(GetDlgItem(IDC_CMB_COMPONENTS));
pColorOptions->SetCurSel(0);
return TRUE;
}
The method FillColorOptions
will fill the combo box. It will iterate over the first dimension of the global matrix. Here's the code:
void CPPCColorConfigDlg::FillColorOptions()
{
CComboBox* pColorOptions;
pColorOptions = reinterpret_cast<CComboBox*>(GetDlgItem(IDC_CMB_COMPONENTS));
pColorOptions->Clear();
for (int i = 0; i < OPTION_VECTOR_SIZE; i++)
{
pColorOptions->AddString(g_vtrShowStrings[i][0]);
}
}
As explained before, the m_iSelected
holds the position of the matrix that is going to be changed. This member is updated when the index of the combo box is changed. Here is the code that responds to such an event:
void CPPCColorConfigDlg::OnSelectOption()
{
CComboBox* pColorOptions;
int iRed, iGreen, iBlue;
pColorOptions = reinterpret_cast<CComboBox*>(GetDlgItem(IDC_CMB_COMPONENTS));
m_iSelected = pColorOptions->GetCurSel();
iRed = iGreen = iBlue = 0;
ParseItemColors(&iRed, &iGreen, &iBlue);
SetColorsInBoxes(iRed, iGreen, iBlue);
DisplayDescription();
}
This method does two actions. First, updates the m_iSelected
member. Second, it will also update the text boxes' values, according to the values within the buffer. The method ParseItemColors
gets the three colors from the memory buffer. The method SetColorsInBoxes
updates the textboxes' values. Here is the code:
void CPPCColorConfigDlg::ParseItemColors(int* pRed, int* pGreen, int* pBlue)
{
int iPosition;
iPosition = m_iSelected * 4;
memcpy(pRed, m_pNewBuffer + iPosition++, 1);
memcpy(pGreen, m_pNewBuffer + iPosition++, 1);
memcpy(pBlue, m_pNewBuffer + iPosition++, 1);
}
void CPPCColorConfigDlg::SetColorsInBoxes(int iRed, int iGreen, int iBlue)
{
CString strRed, strGreen, strBlue;
strRed.Format(_T("%d"), iRed);
strGreen.Format(_T("%d"), iGreen);
strBlue.Format(_T("%d"), iBlue);
SetDlgItemText(IDC_TXT_RED, strRed);
SetDlgItemText(IDC_TXT_GREEN, strGreen);
SetDlgItemText(IDC_TXT_BLUE, strBlue);
}
Notice that in ParseItemColors
, we use m_iSelected
to determine the position within the memory buffer.
When an item is selected from the combo box, it will also call the DisplayDescription
method, which will show a brief description about the system's component. It takes such a value from the global matrix.
void CPPCColorConfigDlg::DisplayDescription()
{
CStatic* pDescription;
pDescription = reinterpret_cast<CStatic*>(GetDlgItem(IDC_LBL_DESCRPT));
pDescription->SetWindowText(g_vtrShowStrings[m_iSelected][1]);
}
When we change the value in one of the three text boxes available, the method OnColorChange
will be called. This method gets such values and update the local ones. Also, it performs some validations, so that the value is always between 0 and 255.
void CPPCColorConfigDlg::OnColorChange()
{
CEdit* pRed, * pGreen, * pBlue;
int iRed, iGreen, iBlue;
CString strRed, strGreen, strBlue;
pRed = reinterpret_cast<CEdit*>(GetDlgItem(IDC_TXT_RED));
pGreen = reinterpret_cast<CEdit*>(GetDlgItem(IDC_TXT_GREEN));
pBlue = reinterpret_cast<CEdit*>(GetDlgItem(IDC_TXT_BLUE));
pRed->GetWindowText(strRed);
pGreen->GetWindowText(strGreen);
pBlue->GetWindowText(strBlue);
iRed = _ttoi(strRed.GetBuffer(0));
iGreen = _ttoi(strGreen.GetBuffer(0));
iBlue = _ttoi(strBlue.GetBuffer(0));
if (iRed < 0 || iRed > 255)
{
MessageBox(_T("Red tone must be between 0 and 255"),
NULL, MB_ICONEXCLAMATION);
pRed->SetWindowText(_T("0"));
}
else if (iGreen < 0 || iGreen > 255)
{
MessageBox(_T("Green tone must be between 0 and 255"),
NULL, MB_ICONEXCLAMATION);
pGreen->SetWindowText(_T("0"));
}
else if (iBlue < 0 || iBlue > 255)
{
MessageBox(_T("Blue tone must be between 0 and 255"),
NULL, MB_ICONEXCLAMATION);
pBlue->SetWindowText(_T("0"));
}
else
{
m_iRed = iRed;
m_iGreen = iGreen;
m_iBlue = iBlue;
InvalidateRect(NULL);
}
}
Notice that in the end, if everything is alright, we call InvalidateRect
. This function will invoke the WM_PAINT
message, so that the window will be redrawn. We then catch such a message within the OnPaint
method, and then we draw a simple rectangle in the window, whose color is the one selected by the user. This will help the user to preview the color that is being selected. The method is simple.
void CPPCColorConfigDlg::OnPaint()
{
CDialog::OnPaint();
CClientDC dc(this);
CBrush brush;
brush.CreateSolidBrush(RGB(m_iRed, m_iGreen, m_iBlue));
dc.SelectObject(&brush);
dc.Rectangle(12, 90, 215, 150);
}
Finally, when we select the system's component and decide its color, we have to push the "Update settings" button. This will call the OnUpdateSettings
method that will update the memory buffer and -eventually- the registry.
void CPPCColorConfigDlg::OnUpdateSettings()
{
int iPosition;
iPosition = m_iSelected * 4;
memcpy(m_pNewBuffer + iPosition++, &m_iRed, 1);
memcpy(m_pNewBuffer + iPosition++, &m_iGreen, 1);
memcpy(m_pNewBuffer + iPosition++, &m_iBlue, 1);
UpdateRegistry();
SetDlgItemText(IDC_LBL_DESCRPT,
_T("A soft reset must be made before changes take effect."));
}
The member UpdateRegistry
... guess what? Updates the registry...
void CPPCColorConfigDlg::UpdateRegistry()
{
HKEY hKey;
DWORD dwType;
DWORD dwSize;
dwType = 0;
dwSize = BUFFER_SIZE;
::RegOpenKeyEx(HKEY_LOCAL_MACHINE, _T("SYSTEM\\GWE"), NULL, NULL, &hKey);
::RegQueryValueEx(hKey, _T("SysColor"), NULL,
&dwType, m_pOldBuffer, &dwSize);
::RegSetValueEx(hKey, _T("SysColor"), NULL,
dwType, m_pNewBuffer, BUFFER_SIZE);
::RegCloseKey(hKey);
}
Conclusions
Well, that's it. You can create all sorts of themes for your Pocket PC. Furthermore, you can include parts of this code in your apps, so you can customize its display, and when the application ends, you could restore the original settings (which are stored in HKEY_LOCAL_MACHINE\System\GWE\DefSysColor). You can even extend this functionality, since you can also change many other keys under HKEY_LOCAL_MACHINE\System\GWE.
Remember that you need to soft reset your device before the changes take effect.
Working with Intermec Devices
If you change the registry to adjust any color as described in the article, the Intermec machine will "reset" your registry to its original value as soon as you soft-reset it. Hence, you will not be able to see the changes. For that, you have two options.
- Remove the file "registry" under the "\Flash File Store" directory.
- Once the registry is updated, and before you soft reset, run the RegFlush2.exe program. This program will take a "picture" of your registry and save it under the "\Flash File Store" directory so the next time it boots, it will take that configuration.
For more information, see the 700c WM 2003 how to save the registry article from Intermec's Knowledge Base.
History
- [Mar 03, 2006] Main release of the article.