Introduction
This is the first of three articles about creating Windows
controls. Article 1 (this) will explain how to design a good control, number 2
goes into coding details by creating a new control step-by-step and article 3
shows the Q&A aspects of controls and how to test and validate Windows
controls.
Here we go�
Designing a Control
I will try to give you some tips how to design a better
control. I have been doing GUI development for almost 10 years now and all of
this writing is out of my own experience.
Many controls, which are available on websites like CodeProject,
are nice pieces of code but not useable at all. At a first glance they seem to
fit perfectly into the project you develop and do all that you need. You
download it and with a little tweaking here and there, you build it into your
application and the Boss is happy that you finished the project in time.
Unfortunately, this is seldom the end of the story when you
develop a commercial application. Even if the new control passes your Q&A
division (you have one, don�t you?), eventually the support team will report
problems.
The most commonly encountered problems with custom controls are:
- incomplete or even missing keyboard handling
- distorted appearance with altered display settings
- non-conformance with the users Windows settings
- incorrect size with altered font size settings
All of this does really happen and it will happen to your
customers. The biggest mistake a developer can make is testing the application
on a developer�s computer. Never ever do this. Take the computer of the
secretary, your aunt�s old 486, or even install a clean OS without the latest
service packs and just choose a different display scheme. Also, test other OS
languages � remember there is a world outside your office.
Tip: most developers do not have the resources to
cleanstall (clean-install) various operating systems on various computers,
there is a simple, yet efficient solution: emulators like VM-Ware (http://www.vmware.com [^]). They have a
reasonable price and are perfectly suited for a developer�s testing
environment.
These mistakes are the result of the fact that (almost) all
these controls are developed for a specific purpose. End of the line. Few
programmers go the extra mile to complete the design and deliver a fully
functional control that will pass all tests. Even many commercial GUI libraries
fail here.
Further reading on controls and control design:
http://msdn.microsoft.com/library/en-us/shellcc/platform/commctls/common/user.asp
http://msdn.microsoft.com/nhp/default.asp?contentid=28000443
Choose Your Weapons Wisely
When you intend to write your own control, stop before you
write a single line of code and figure out if you really need to create a
custom control or if it can be solved with a standard Windows control or a
combination of them.
Windows provides a rich variety of controls, which are
suited for almost any purpose and they are 99% bug free. Your custom control
will reach this level only after many, many hours of use, testing and
debugging. Often it requires a change in design to achieve the required results
with the standard controls, but normally it is worth the change. You gain
stability and standard conformance. Your users do not need to learn how to use
your new control; they are familiar with the Windows ones. If you think that
your users must have a button with a different background color and another
font, go out and ask your users. They may like the colorful appearance, but if
you confront them with a standard solution, they will agree that the standard
solution is better to use.
Remember: Usability and customization seldom go hand in hand.
Ok, so you decided you must have an extraordinary whatever-control?
Fine, lets go ahead and figure out how to do it right.
Figure out what is the target audience, the average user, of
your application. This determines how you represent your data, how you label
it, how it has to look.
Let us take an (bad) example: a color chooser. Most
color chooser controls offer various methods to select or manipulate a color.
One type of representation is the HSB/HSL model. In this (and other) model, the
color is represented by three numeric values, named H, S and B (Hue, Saturation
and Brightness). Now ask yourself, how many average home users know what this
three letters mean?
�Look at the picture. This
shot is taken from a widely used text processing application. Is �Sat:� a
meaningful label? (Besides, in this specific application there would be even
enough space to write the complete word instead of an abbreviation.)
If developers think before they write, they might come
up with a better, more intuitive solution.
Ok, back to our design. Now that you know your audience, you
can make a clear decision on the representation. Always keep in mind, Joe
Averageuser does not want a cool looking control, but one that is easy to
handle, that he understand it without pressing F1 or reading the manual. If it
satisfies these requirements and is still cool looking, then you are one-step
further on your bosses raise list.
A word about the keyboard: handle keys like Windows does �
up is up and down is down. If you use the Enter key to select an item, your
users will not understand it. MSDN has nice lists on keys and their suggested
use.
Before you start coding, make a mockup of your control. Fire
up your favorite paint program and sketch the control. Place the bitmap in your
application. See how it looks and ask others what they think of it. Do they
understand what it is? How would they use it?
If your dummy survives this simple test, you can � almost �
start coding.
�Captain, focus ahead�
Now it is time to think about the controls usage, its
navigation. In most cases, you know instinctively how to use it with a mouse. However,
what about the keyboard? Many users (that includes me) like to use the keyboard
for navigation, just because it is faster, just because you have to type
something in a nearby field, or simply because the computer does not have a
mouse attached.
Keyboard navigation always includes showing a focus. The
focus is a visual feedback to indicate which part of your control the user is
interacting with. This is the user�s only clue what he is doing. Again, look at
the Windows controls to see how a focus is shown. The button shows a dotted
rectangle and changes border, an edit control shows a caret at the insert
position, a menu shows a colored selection. Imitate this behavior. Joe
Averageuser is used to it.
I want to show you a small flaw in a Windows control,
which confuses almost any (keyboard-) user and looks like a �forgotten�
feature: the focus rectangle in the Listview control. When it contains items,
but none of the items are selected and the user navigates to the list by using
the tab key, the list does receive the focus, but it does not show the focus
rectangle. To the user it looks like no control has the focus. A simple
solution is to intercept the WM_SETFOCUS
message and set the LVIS_FOCUS
state on the first list item if no item is selected or
focused.
If you do not get it right at first, don�t worry � even
Microsoft doesn�t always. (MS: How are we supposed to use the dropdown function
of the �Open� button in Word XP�s File Open dialog?) Always ask others for
their opinion about your control and if they understand it. Watch them using
it.
Don�t forget that you have to remove the focus if your
control or the application looses focus. There are certain windows messages,
which you should handle because they may change focus state or style. To make
things more complicated, these messages vary on different Windows versions.
Focus management means keeping some state variables around
and up-to-date. This can be a tricky part of your controls logic. Depending on
the complexity of your control you might even need to show focus in various
forms � a caret when the user interacts with text, a dotted line on a
button-type area, etc.
Another important visual feedback for focus � mouse focus
this time � is hot tracking. Hot tracking means changing the visual appearance
of an item or area when the user moves (hovers) the mouse over it. A good
example, are the buttons in the toolbar of Internet Explorer. Here, the search
button normal and hot-tracked.
When you implement features like this, adhere to the Windows
settings and capabilities of the installed OS and IE version. This will make
your control appear much more �Windows like� because it acts and reacts like
all other standard controls.
The Windows API functions GetSystemMetrics()
and
SystemParametersInfo()
are your best friends. They provide you all information that you need. If you
cache this information in your own variables, you have to process the change
notifications sent by Windows whenever the user changes a setting. Look at the WM_xxxCHANGED
notifications
in your MSDN.
Ma�, why is it so big?
Do you calculate the size of your control in pixel? Do you
draw at certain locations? Don�t do it. When you place your control on a
dialog, it is measured in dialog units. Dialog units scale depending on the
display settings. When the user chooses a big default font, the dialog will be
bigger and so will your control. Try it; select �Large Fonts� in the display
properties. How does your control look now?
Always calculate sizes and locations relative. If you are
going to display simple iconic graphics, consider using a TrueType font.
Windows also uses a font to display the symbols on any window. The Minimize,
Maximize, Close icons but also the scroll arrows are just letters from a custom
font. Outlook uses a custom font for the attachment, priority and other
symbols. Why? Because it scales. Bitmaps don�t scale. And it is yet ten times
easier to output text than display a bitmap or even WMF file.
If you create items, which relate to Windows items, mimic
Windows. Use GetSystemMetrics()
to retrieve the size, dimensions or depth of the corresponding Windows element.
For example, when you draw a 3D element, you can get the correct size by calling
GetSystemMetrics(SM_CXEDGE)
and GetSystemMetrics(SM_CYEDGE)
.
This ensures that your control looks right in all configurations and Windows
versions.
When you must display bitmaps or icons then be very careful with
transparency and background colors. Often programmers forget that the
background color of an icon is not gray. You can notice this in many
pre-Windows 2000 applications when Microsoft used a different gray for the
background of dialogs and windows. When you run these applications with altered
colors (or under Windows 2000/XP) then you notice that the icons have a
different gray.
So, make the background color transparent and set the
transparency color correctly. You can easily check if you got it right, by
changing the color of the 3D Objects in the display settings.
Never assume a color because the user will change it. Always use GetSysColor()
and GetSysColorBrush()
to get the currently active colors. What I said about the metrics, applies also for the colors: they may be changed at any time. Make use of the WM_xxxCHANGED
notifications.
HWND, WPARAM and LPARAM
You cannot post virtual functions.
How are programmers going to use your control? Through a
class, which implements the control, you will answer.
Not exactly the right answer. I know, it�s the common and
most convenient way. But just look at the Windows controls � MFC programmers
often forget that there is no function named SetExtendedStyle()
in the Windows API.
This is just the MFC class function which makes a simple SendMessage()
with some
parameters.
It takes some effort to implement a message-based interface
instead of a class-based interface. But it has several advantages:
- asynchronous calls (using
PostMessage
)
- subclassing it and modifying/extending the behavior without source code
- tracing messages with tools like Spy++ or Winspector[^]
- integration with other programming languages and tools
Let us have a look at the conventional implementation:
class CMyControl
{
void SetColor(RGB c) { m_color = c; Invalidate(); }
RGB GetColor() { return m_color; }
private:
RGB m_color;
};
That is all that is. End of story. While this is the easiest
and fastest implementation, it is also the most inflexible one. Even when you
declare the SetColor/GetColor
functions as virtual
you don�t gain much. None of the advantages of a message-based implementation
is here.
Now, the same in a message-based implementation:
#define MC_SETCOLOR (MC_BASEMSG + 1)
#define MC_GETCOLOR (MC_BASEMSG + 2)
class CMyControlImpl
{
void SetColor(RGB c) { SendMessage(MC_SETCOLOR, 0, (LPARAM)c); }
RGB GetColor() { return SendMessage(MC_GETCOLOR); }
afx_msg LRESULT OnMsgSetColor(UINT, WPARAM, LPARAM);
private:
RGB m_color;
};
BEGIN_MESSAGE_MAP(CMyControlImpl, CWnd)
ON_MESSAGE(MC_SETCOLOR, OnMsgSetColor)
ON_MESSAGE(MC_GETCOLOR, OnMsgGetColor)
END_MESSAGE_MAP()
LRESULT CMyControlImpl::OnMsgSetColor(UINT, WPARAM c, LPARAM)
{
m_color = (RGB)c;
Invalidate();
}
LRESULT CMyControlImpl::OnMsgGetColor(UINT, WPARAM, LPARAM)
{
return (LRESULT) m_color;
}
This is quite a lot more code to write. But any developer
using this control will need only the first file MyControl.h which contains all
message definitions. If your control is complex or you have enough time and
motivation at hand, then you provide a wrapper class and/or macros.
#define MyControl_SetColor (hwndMC, c) \
(int)SNDMSG((hwndMC), MC_SETCOLOR, (WPARAM)(c), 0L)
#define MyControl_GetColor (hwndMC) \
(int)SNDMSG((hwndMC), MC_GETCOLOR, 0, 0L)
class CMyControl
{
public:
void SetColor(RGB c) { MyControl_SetColor(m_hWnd, c); }
RGB GetColor() { return MyControl_GetColor(m_hWnd); }
}
This is exactly what MFC is doing (with a little more error
checking in-between) with all the standard Windows controls. Have a look at the CEdit
class in the MFC source files. Learn how they did it. Also, look into the commctrl.h
file in the VC
include directory. This file contains the definitions for all Windows controls.
Using this method, you gain all advantages mentioned before.
It is more work because of all the definitions and extra header files. In the
end, you cannot avoid this extra step when you want to create commercial
quality controls.
If you ever wrote controls, you will know the pain of
debugging GUI code. With the message-based approach things get easier � attach
Spy++ or even better Winspector[^] to your control and see the messages flow. You
can easily spot any wrongly sent message, any out-of-range parameter and any
return value.
One last point to enhance your control: do not call exposed
functions directly. Let me demonstrate this with our sample.
Assume we have another function called UpdateData()
, which at some
point has to set the color and thus either calls the OnMsgSetColor()
directly or even worse,
modifies the m_color
variable. You will never be able to figure out why the control changes color,
if you don�t trace into the UpdateData()
function. Would this function send a MC_SETCOLOR
message, it would be obvious why the color
change happens.
Of course, for more complex functions you might need to define structures which are passed by pointer to the control. Even if you have
to pass more then 2 parameters you will need to squeeze them into a structure already or you manage to use short int
parameters and pass them with the�MAKEWPARAM()
or MAKELPARAM()
macros.
Who Would �Print� a Control?
Well, most probably not many people do this. I agree that �print� is not the best choice of words for the purpose. Something like �render� would have been more appropriate.
By now, you may have realized that I am talking about the WM_PRINT
and WM_PRINTCLIENT
messages. They are not much different from the normal WM_PAINT message, but they may be invoked without an invalid area. At any time, the application can request a control to paint itself into a given DC by sending a WM_PRINT
or WM_PRINTCLIENT
message.
To implement these messages you only have to move your painting code into a separate function, which takes a DC as input parameter. The difference between WM_PRINT
and WM_PRINTCLIENT
is that the first should render the whole control window including non-client areas and the later should only render the client area.
Implementing these messages is required to support some of the newer Windows API functions like AnimateWindow()
.
Terminal Services Awareness
Terminal services are no longer a buzzword but reality for many companies and all Windows XP users. They are available by default on any Windows XP installation just that they are now called Remote Desktop. Also available as Windows NT 4.0 Terminal Services and Citrix Terminal Server and probably under some other names. Do not underestimate how widely used this is in enterprises. This is definitely a feature you want to add to your windows control.
There are few things to consider when you plan to make your control TS aware: the painting code must be optimized and mouse interaction should be minimized. By detecting TS sessions and handling the WM_WTSESSION_CHANGE
message, the control can minimize graphic effects when used in a terminal session. Advanced features like hovering can be disabled; the ever-popular flat controls, which show/hide the border when active, may revert to normal operations, and so on.
With careful planning, TS awareness can be added without breaking compatibility to Win9x systems.
The Modern Architecture Thingy
With each new Windows version, Microsoft adds some new features to the existing controls, new windows messages, new styles. Most of these features are easy to implement if you plan your control carefully.
One of these new things is the UI status feedback. You can turn it on or off in Display Settings / Appearance / Effects. It is titled �Hide underlined letters for keyboard navigation until I press the Alt key�. The context help of this item already reveals that there is more to it than they tell with this clunky title.
This special feature is hidden behind three new windows messages: WM_QUERYUISTATE
, WM_UPDATEUISTATE
and WM_CHANGEUISTATE
. These messages simply take or give a bitmap specifying which feature should be turned off or on. The only thing your control has to do is consider these bits when drawing the control or its visual feedback.
Another big thing is Windows XP�s Theme support. Fully supporting themes is lots of work, if your control uses default windows elements (buttons, scrollbars, dropdown indicators, etc) as parts of the window, you must implement Theme support. There is no way around. Otherwise, your control looks awful between all the themed controls in your customer�s application. Fortunately, there are a few helper classes on sites like CodeProject, which you can easily use in your implementation.
Conclusion
Writing good controls is hard work. Designing a controls
interface takes time. A control that you created for your own application will
never be a complete solution. It just implements what you needed. The next
programmer using your control may have very different requirements to the
interface or environment. If you intend to publish your control, review the
interface and code carefully and add functionality that is missing.
Any Set�
function should have a corresponding Get�
function.
Even if you do not need it in your application, others will need. When your control provides the function already,
then it is reusable. Otherwise, it is just a code fragment, which needs massive work.
If your control uses constants or #define
d measures, think if they can be made configurable. Do not impose limits just because you did not need more.
That�s all for now. I will update this article based on your
feedback and any further ideas. I hope you will read on when the next part of
this series is written.