<?xml version="1.0" encoding="UTF-8" ?>
<?xml-stylesheet type="text/xsl" href="http://blogs.msdn.com/utility/FeedStylesheets/rss.xsl" media="screen"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:wfw="http://wellformedweb.org/CommentAPI/"><channel><title>Chris Jackson's Semantic Consonance : User Experience</title><link>http://blogs.msdn.com/cjacks/archive/tags/User+Experience/default.aspx</link><description>Tags: User Experience</description><dc:language>en-US</dc:language><generator>CommunityServer 2.1 SP1 (Build: 61025.2)</generator><item><title>How Much Personalization Should Your Software Support?</title><link>http://blogs.msdn.com/cjacks/archive/2006/07/10/661089.aspx</link><pubDate>Mon, 10 Jul 2006 09:05:20 GMT</pubDate><guid isPermaLink="false">91d46819-8472-40ad-a661-2c78acb4018c:661089</guid><dc:creator>Chris Jackson</dc:creator><slash:comments>7</slash:comments><comments>http://blogs.msdn.com/cjacks/comments/661089.aspx</comments><wfw:commentRss>http://blogs.msdn.com/cjacks/commentrss.aspx?PostID=661089</wfw:commentRss><wfw:comment>http://blogs.msdn.com/cjacks/rsscomments.aspx?PostID=661089</wfw:comment><description>&lt;p&gt;In my last post, I took a look at one specific element of the design in Windows Vista. My intention was to explore two themes that I think this one small example illustrated well: the importance of telling a consistent story well, and the importance of understanding the human emotional response to a user interface.&lt;/p&gt;
&lt;p&gt;A couple of comments targeted particular pet peeves about the particular design element in question, which unfortunately wasn't my target because I don't really have the ability to change it with anything more than a bug report (which I am happy to submit on behalf of my readers), but it makes for a good discussion nonetheless. I want to focus in on one in particular, because I believe it really targets a broader and more interesting discussion on user interface design.&lt;/p&gt;
&lt;cite&gt;Dispite the fact that it makes sense (in a small way it does), there should be an option to allow for being either still transparent or opaque when maximized. I believe one of the many reasons that some/alot of people don't like Windows is because there's not enough customizable options. Sure, there are some, but there are options unavailable where they should be; such as the example you just emphasized. When comes to the UI, judging from working with computers for 15 years, people want it to be 'fully' and not 'partially' customizable (remember something called Linux :)....'nuff said.&lt;/cite&gt;
&lt;p&gt;I want to dissect this assertion because it gave me some interesting things to think about.&lt;/p&gt;
&lt;p&gt;First, there is a hidden claim, assumed to be true, that "some/alot of people don't like Windows." If we want to try to understand the reason why something is true, from a scientific perspective, we first want to understand that it is true. And what do we need for that? Data. I have not seen a scientific study that sought to determine the emotional response to the product, and how people feel while using it (although for all I know this could certainly have been done many times over). Note, however, that the emotional response to the pure aesthetics can be divorced from the emotional response driven by daily usage. What looks startlingly beautiful in a screenshot may be overbearing and annoying during constant use. Remember the Pepsi challenge, where people reacted strongly to the sweet taste of Pepsi in a sip test because we are evolutionarily programmed to favor sweet things; in higher quantities (read: normal usage - when was the last time you drank 2 ounces of our soda and tossed the rest away, not craving another soda for a few days?) the data looked significantly different.&lt;/p&gt;
&lt;p&gt;For the moment, let's overlook this assumption. After all, if everyone already had a perfect emotional response to Windows, we wouldn't be working so hard to make another one, would we?&lt;/p&gt;
&lt;p&gt;The larger claim is making an assertion as to why this might be the case, zeroing in on customization. This comment suggests that it's just not customizable enough. If I flip through the options in Windows, I see a whole lot of customization. Tons and tons of it. Control panel applets that branch into more specific applets, many of which offer a popup advanced window that can even allow you to change the behavior of the power button on the start menu differently depending on whether or not you're running on batter power. A host of options in gpedit.msc that can even be deployed across an entire enterprise. Raymond Chen's famous TweakUI. There is a LOT of customization available. And with it, a huge amount of complexity.&lt;/p&gt;
&lt;p&gt;So, if we want to take anecdotal evidence, my experience is completely different. Until this comment, I have never once heard somebody suggest that lack of sufficient customization was an issue in Windows whatsoever. In fact, complexity (which is what customization necessarily brings) would be one of the biggest issues. Reliability. Security. All of which the team is working hard on in Windows Vista. But never once customization. Similarly, I have never once heard somebody who uses one of our competitors' products claim that they do so because of the customizability of the product. John Hodgman has never once been ridiculed for not being customizable enough. (Incidentally, if you are a Daily Show fan, be sure not to miss &lt;a href="http://www.microsoft.com/college/content/college.wvx"&gt;Ed Helms' on-scene report&lt;/a&gt; on Microsoft.)&lt;/p&gt;
&lt;p&gt;Without any detailed research findings exploring motivation behind purchasing decisions, let's instead just go through a thought exercise around customizability and personalization. I am not limiting this to Windows, mind you. After all, you probably don't design and implement the UI for Windows, and neither do I. Rather, let's take the general case. For any software that you or I may write, how personalizable should it be?&lt;/p&gt;
&lt;p align="center"&gt;&lt;img src="http://blogs.msdn.com/photos/cjacks/images/661017/original.aspx" /&gt;&lt;/p&gt;
&lt;p align="center"&gt;&lt;cite&gt;How many personalization knobs and switches should you have?&lt;/cite&gt;&lt;/p&gt;
&lt;p&gt;We tell you to provide personalization in your applications. &lt;a href="http://msdn.microsoft.com/library/en-us/dnwue/html/ch02b.asp"&gt;"Because of their widely varying skills and preferences, users must be able to personalize aspects of the interface."&lt;/a&gt; It is easy to interpret this statement as being as sweeping as it sounds, and honestly I do not know the intent of the author. However, I would be cautious about taking this statement too far.&lt;/p&gt;
&lt;p&gt;First of all, even being the most sophisticated user of an application does not necessarily endow you with user interface design skills. To quote Jef Raskin, "The central point of this issue is that if we are competent user interface designers and can make our interfaces nearly optimal, personalizations can only make the interface worse."&lt;/p&gt;
&lt;p&gt;It is also important to consider the ramifications of extensive personalization. &lt;a href="http://blogs.msdn.com/jensenh/archive/2006/03/02/542118.aspx"&gt;Jensen Harris discusses how personalization can lead to a degrading user experience with Microsoft Office in his blog.&lt;/a&gt; The user must learn both how to use the software and how to personalize it, and personalizing it frequently does not lead to increased productivity (particularly if you begin with a nearly optimal design). Documentation becomes harder to create. Screenshots may not match what you see on your screen. If you are trying to walk a friend through using the software, you may direct them to use a feature which has been customized to be hidden, so now you not only have to help them learn how to use that feature, you end up teaching them how to use the personalization features before you can even get to that. If you end up accidentally personalizing, you can be left in a mode you simply don't know how to get out of. There are important side effects to consider for providing personalization features.&lt;/p&gt;
&lt;p&gt;Of course, the counter argument is that your "personal" computer really should be personal if you want it to be. While most people want their operating system to really fade to the background and not even be noticable (which is what Windows Vista strives to do - your applicaitons should be the focus) and don't give it a second thought, there certainly is a percentage of the population that wants their operating system to shout loudly a personal statement about them. I have intermitently been among this group, so I get that. &lt;a href="http://www.stardock.com/"&gt;Stardock&lt;/a&gt; sells their Object Desktop line of products precisely to fill this need. And I think this makes for a very complete story. Most people want to have a modern, crisp UI that does the job well, feels nice to use, and really does a good job of staying out of the way because they don't care about their operating system, they care about their work or their fun. For those that want bling, we have partners who deliver.&lt;/p&gt;
&lt;p&gt;What does that mean for our applications? Well, if we are designing line of business software, we should design our software to be optimized for the task at hand. (Not that they should be ugly - being optimized for the job at hand, to me, implies driving as positive an emotional reaction as possible, and not just providing the correct number of input boxes.) If we are designing commercial software for consumers, we should take the same route, but we may also want to consider which personalizations may add value. And, once we have done that, we really need to make a well designed personalization system and account for the time to document and explain the intentional modality we have introduced. Windows Media Player provides an interesting example. The documentation will always refer to the full sized window. You can make an arbitrary skin that looks like a giant eyeball, but you can always press the "get me out of this wacky eyeball mode" button and find yourself back in a state that completely matches the documentaiton. And, personally, once the full mode had a well optimized UI, I never went to skin mode again.&lt;/p&gt;&lt;img src="http://blogs.msdn.com/aggbug.aspx?PostID=661089" width="1" height="1"&gt;</description><category domain="http://blogs.msdn.com/cjacks/archive/tags/User+Experience/default.aspx">User Experience</category></item><item><title>Why Did My Windows Vista Glass Go Away When I Maximized My Window?</title><link>http://blogs.msdn.com/cjacks/archive/2006/06/21/641735.aspx</link><pubDate>Wed, 21 Jun 2006 20:09:46 GMT</pubDate><guid isPermaLink="false">91d46819-8472-40ad-a661-2c78acb4018c:641735</guid><dc:creator>Chris Jackson</dc:creator><slash:comments>9</slash:comments><comments>http://blogs.msdn.com/cjacks/comments/641735.aspx</comments><wfw:commentRss>http://blogs.msdn.com/cjacks/commentrss.aspx?PostID=641735</wfw:commentRss><wfw:comment>http://blogs.msdn.com/cjacks/rsscomments.aspx?PostID=641735</wfw:comment><description>&lt;p&gt;Up front notice: I am neither the designer nor the developer of the window chrome in Windows Vista.&lt;/p&gt;
&lt;p&gt;I have been using Windows Vista Beta 2 as my primary operating system since it was released. My overall impression so far: it is really, really tight. Sure, there are still some warts. Sure, I have to relearn a number of things that at one point I could do without thinking. But overall, it really feels good.&lt;/p&gt;
&lt;p&gt;Of course, my purpose in posting is not to shower praise on the product. Give it a try yourself and form your opinions. Rather, I wanted to take one small aspect of the design and dissect it from my own, very personal, point of view. It illustrates to me very clearly how important the little details are in implementing a great design.&lt;/p&gt;
&lt;p&gt;One of the most clearly (bad pun, I know) visible aspects of the UI design is that the windows are partially transparent. Not quite transparent, really - they actually distort the image behind them (using pixel shaders).  However, as soon as you maximize a window, the transparency goes away. Why?&lt;/p&gt;
&lt;p align="center"&gt;&lt;img src="http://blogs.msdn.com/photos/cjacks/images/641640/original.aspx" /&gt;&lt;/p&gt;
&lt;p align="center"&gt;&lt;cite&gt;Windows Vista Beta 2 Glass Windows: Maximized (top) and Normal (bottom)&lt;/cite&gt;&lt;/p&gt;
&lt;p&gt;To understand this decision, it is important to think about the story that the designers are trying to tell with the glass windows. What is their purpose? Is it just to look cool? In this case, looking cool is certainly part of the story, but I suspect the real intention here is to keep the window chrome out of the way. This story is completely consistent with the behavior we are seeing. When in normal mode, the window chrome (which we add on to windows) gives you access to key window management functionality, but other than that it just just extra stuff that blocks your view of other items on your desktop. By making it get out of the way somewhat, it blocks those items less. However, if it were completely transparent or undistorted, then you could see the items behind it a little bit too clearly, thus raising their importance so they begin to get in the way. It is a delicate balance - reduce the heaviness of the chrome, but don't reduce it so much that other items start to become distractions. Whether they have it perfectly balanced is in the eye of the beholder, and in fact the control panel will let you tweak that balance if they didn't nail it to your liking.&lt;/p&gt;
&lt;p&gt;When you maximize a window, however, you are communicating a very different intent. You no longer need to have the ability to maneuver windows around with these light weight frames. How do we know that? Because you can't maneuver windows around while they are maximized! If the window chrome started showing what was behind that window, it would add new things to get in the way, with no real purpose for showing these things as a means of getting out of the way. Reducing the brightness and removing the transparency when maximized is a clever way of following the same principal - the window chrome should be secondary to the content of that window. It should stay out of the way in the most appropriate way, based on the user's intent. Clever.&lt;/p&gt;
&lt;p&gt;Clever, but not perfect.&lt;/p&gt;
&lt;p&gt;If it were perfect, you see, nobody would ever ask that question, because it would be inherently obvious what was happening and why.&lt;/p&gt;
&lt;p&gt;How could it better communicate the rationale? My first thought - what if it animated? As it is right now (which, admittedly, could be due to some very early WDDM drivers), the window chrome literally snaps from glass to opaque. What if this were animated? Then, we could see what was happening and increase our likelihood of understanding correctly. If the alpha faded away and the luminance decreased smoothly, then it might do a better job of sending the message, "Hey, I see that there is no more reason for me to show you what is behind me, so I'll go ahead and get out of the way now."&lt;/p&gt;
&lt;p&gt;When I think of animation, this is the type of communication that I think it is best used for. Yes, with Windows Presentation Foundation, you can make buttons bounce arbitrarily around the screen with almost no code. But why would you ever want that? However, when you use animation to communicate rather than just look cool, then you have hit the sweet spot.&lt;/p&gt;
&lt;p&gt;What else do I find imperfect? This is a bit more subtle, and deals with my personal response to the user interface.&lt;/p&gt;
&lt;p&gt;The human mind is optimized for pattern matching. We are consistently anthropomorphising, consistently seeing metaphors. When I maneuver glass windows around my desktop, I don't just see the pixels. I personally see (feel?) an application drawing surface resting on a very high quality glass frame in 3-dimensional space. I really like the feel of the material that my brain is inferring. It feels expensive. When I maximize a window, it literally feels to me as if it has changed material. The texture is different. Rather than this expensive glass, now it feels more like plastic. Even more, I begin to see the pixels at the top of the frame as pixels instead of as natural shading and highlighting, causing me to stop suspending my disbelief in the metaphor my mind has created.&lt;/p&gt;
&lt;p&gt;My logical mind tells me that the "texture" my brain is seeing is caused by the pixel shaders, and not by anything that is explicitly rendered on the chrome. As soon as you take the pixel shaders away (for a sound design reason, mind you), how can you maintain that sense of texture? I don't know. What I do know, however, is that I like the "feel" of that chrome enough that it changes my behavior. Rather than maximize my windows, which leaves me with the cheap plasticy frame with pixels drawn on it, I leave my windows restored and just stretch them to fill my desktop. I like the feel of the material that much.&lt;/p&gt;
&lt;p&gt;What can I learn from this? First, you can see just how important it is to tell a story and communicate. The window chrome in Windows Vista Beta 2 tells such a story. Animation can help tell that story, and it should be used when it has the power to communicate. Most importantly, I learned never to underestimate how emotions can influence the experience of your user interface. It is more than about pixels. It is about manipulating an experience and leveraging the pattern seeking tendencies of the human brain to make people feel good about software (or, at a minimum, avoid making them feel bad). Logic does not always trump emotions.&lt;/p&gt;&lt;img src="http://blogs.msdn.com/aggbug.aspx?PostID=641735" width="1" height="1"&gt;</description><category domain="http://blogs.msdn.com/cjacks/archive/tags/User+Experience/default.aspx">User Experience</category></item><item><title>Light Up Your Fonts on Vista: Selecting Segoe UI Using GetThemeSysFont, NONCLIENTMETRICS, or SystemFonts.MessageBoxFont</title><link>http://blogs.msdn.com/cjacks/archive/2006/06/02/614575.aspx</link><pubDate>Fri, 02 Jun 2006 22:32:30 GMT</pubDate><guid isPermaLink="false">91d46819-8472-40ad-a661-2c78acb4018c:614575</guid><dc:creator>Chris Jackson</dc:creator><slash:comments>2</slash:comments><comments>http://blogs.msdn.com/cjacks/comments/614575.aspx</comments><wfw:commentRss>http://blogs.msdn.com/cjacks/commentrss.aspx?PostID=614575</wfw:commentRss><wfw:comment>http://blogs.msdn.com/cjacks/rsscomments.aspx?PostID=614575</wfw:comment><description>&lt;p&gt;Last time around, I suggested, "Let's see if I can talk about something other than rendering text next time around..." It looks like the answer to that one is no.&lt;/p&gt;
&lt;p&gt;Windows Vista has reached Beta 2, and we are already beginning to see documentation around &lt;a href="http://msdn.microsoft.com/windowsvista/default.aspx?pull=/library/en-us/dnlong/html/vistatopten.asp"&gt;lighting up your applications on Windows Vista&lt;/a&gt;. One of the ways to make your application feel more like Vista is to use fonts consistent with the UI: Segoe UI. The &lt;a href="http://msdn.microsoft.com/library/default.asp?url=/library/en-us/UxGuide/UXGuide/Visuals/Fonts/Fonts.asp"&gt;fonts&lt;/a&gt; section of the &lt;a href="http://msdn.microsoft.com/library/default.asp?url=/library/en-us/UxGuide/UXGuide/Visuals/Fonts/Fonts.asp"&gt;Windows Vista User Experience Guideliness&lt;/a&gt; discusses using Segoe UI - the system font. This documentation specifies, "From code, you can determine the system font properties (including its size) using the GetThemeFont API function."&lt;/p&gt;
&lt;p&gt;Unfortunately, this is all of the direction that it provides. Anybody who has played around with the GetThemeFont API realizes that this is easier said than done. There are a respectably large number of permutations of classes, parts, and states, and anybody who is working in managed code will need to rifle through include files from the Platform SDK to locate all of the values to pass in. Once you have that working, you will then discover that the vast majority of permutations do not contain a font value at all, and the documentation is pretty much silent on the topic of which values to use to improve your chances of success, making this all the more challenging to the developer.&lt;/p&gt;
&lt;p&gt;You can avoid a significant amount of p/invoke code by using the Visual Styles API - the VisualStylesRenderer class provides a GetFont method that wraps the unmanaged GetThemeFont API for you, but you are still left with the problem of locating a permutation of class, part, and state that return a Font at all, let alone the one you are hoping to use!&lt;/p&gt;
&lt;p&gt;Personally, I find it much easier to use the GetThemeSysFont API to retrieve the system font. With this API, you specify whether you want to retreive the caption, small caption, menu, status, message box, or icon title font. Personally, I use the message box font, given that this font is also designed to display readable, sentence-formatted text. Lets' take a look at how this looks:&lt;/p&gt;
&lt;p align="center"&gt;&lt;img src="http://blogs.msdn.com/photos/cjacks/images/614431/original.aspx" /&gt;&lt;/p&gt;
&lt;p align="center"&gt;&lt;cite&gt;GetThemeSysFont on Windows XP SP-2&lt;/cite&gt;&lt;/p&gt;
&lt;p align="center"&gt;&lt;img src="http://blogs.msdn.com/photos/cjacks/images/614433/original.aspx" /&gt;&lt;/p&gt;
&lt;p align="center"&gt;&lt;cite&gt;GetThemeSysFont on Windows Vista Beta 2&lt;/cite&gt;&lt;/p&gt;
&lt;p&gt;The source code to construct this simple example follows:&lt;/p&gt;
&lt;pre&gt;
namespace GetThemeSysFont {

    using System;
    using System.Drawing;
    using System.Runtime.InteropServices;
    using System.Windows.Forms;
    using System.Windows.Forms.VisualStyles;

    public partial class Form1 : Form {

        #region p/invoke declarations

        [DllImport("uxtheme.dll", ExactSpelling = true,
            CharSet = CharSet.Unicode)]
        private static extern IntPtr OpenThemeData(IntPtr hWnd,
            String classList);

        [DllImport("uxtheme", ExactSpelling = true,
            CharSet = CharSet.Unicode)]
        private extern static Int32 GetThemeSysFont(IntPtr hTheme,
            int iFontId, out LOGFONT plf);

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        private struct LOGFONT {
            public int lfHeight;
            public int lfWidth;
            public int lfEscapement;
            public int lfOrientation;
            public int lfWeight;
            public byte lfItalic;
            public byte lfUnderline;
            public byte lfStrikeOut;
            public byte lfCharSet;
            public byte lfOutPrecision;
            public byte lfClipPrecision;
            public byte lfQuality;
            public byte lfPitchAndFamily;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
            public string lfFaceSize;
        }

        private const int TMT_CAPTIONFONT = 801;
        private const int TMT_SMALLCAPTIONFONT = 802;
        private const int TMT_MENUFONT = 803;
        private const int TMT_STATUSFONT = 804;
        private const int TMT_MSGBOXFONT = 805;
        private const int TMT_ICONTITLEFONT = 806;


        #endregion

        public Form1() {
            InitializeComponent();
            Font themeFont = messageBoxFont();
            platformLabel.Font = themeFont;
            platformLabel.Text = Environment.OSVersion.VersionString;
            themeFontLabel.Font = themeFont;
            themeFontLabel.Text = "Message Box Font: " + themeFont.Name +
                " " + themeFont.SizeInPoints + "pt";
        }

        private Font messageBoxFont() {
            // You can avoid p/invoke altogether in managed code
            // using the following:
            // return SystemFonts.MessageBoxFont;
            LOGFONT pFont;
            IntPtr hTheme = OpenThemeData(this.Handle, "WINDOW");
            GetThemeSysFont(hTheme, TMT_MSGBOXFONT, out pFont);
            return Font.FromLogFont(pFont);
        }
    }
}
&lt;/pre&gt;
&lt;p&gt;This approach will work well in both managed code and unmanaged code. Unfortunately, I am not aware of a wrapper in the managed Visual Styles APIs for GetThemeSysFont, so I went the p/invoke route.&lt;/p&gt;
&lt;p&gt;Are there other alternatives to getting the system font? Yes there are! (If you read the commented out code, you will even see one - we'll get to that one in a bit.)&lt;/p&gt;
&lt;p&gt;Well, we are getting the message box font. If you poke around the NONCLIENTMETRICS structure you can retrieve from a call to SystemParametersInfo, you will notice that there is a lfMessageFont LOGFONT that returns the message box font. This works in both managed and unmanaged code, and has the distinct advantage of not taking a dependency on uxtheme.dll - which you will only find on Windows XP and above.&lt;/p&gt;
&lt;p&gt;What about nice shortcuts in the managed world? The SystemFonts class, by its very name, suggests that it will return the correct system font. Unfortunately, if you go with the most obvious choice, the DefaultFont property, you will be handed a font that is very clearly not Segoe UI. What has happened here? Well, internally, the DefaultFont property has a very strong tendency to return the hard-coded value of Tahoma (depending on your locale). Otherwise, you get the outcome GetStockObject(DEFAULT_GUI_FONT). Wait a minute, the DEFAULT_GUI_FONT? The one &lt;a href="http://blogs.msdn.com/oldnewthing/archive/2005/07/07/436435.aspx"&gt;Raymond Chen describes as, "DEFAULT_GUI_FONT has an even less illustrious history. It was created during Windows 95 development in the hopes of becoming the new default GUI font, but by July 1994, Windows itself stopped using it in favor of the various fonts returned by the SystemParametersInfo function. Its existence is now vestigial."&lt;/a&gt;? Yes, that's the one.&lt;/p&gt;
&lt;p&gt;OK, so DefaultFont isn't a particularly good choice. That's unfortunate, because it just "feels" like it should be the right choice.&lt;/p&gt;
&lt;p&gt;However, the MessageBoxFont property does, indeed, wrap the SystemParametersInfo function with an argument of NONCLIENTMETRICS, giving you a straightforward way to go after this font without having to use p/invoke.&lt;/p&gt;
&lt;p&gt;So, in the end, MessageBoxFont is probably the easiest option to go after Segoe UI on Vista using managed code, while providing acceptable results on existing platforms.&lt;/p&gt;&lt;img src="http://blogs.msdn.com/aggbug.aspx?PostID=614575" width="1" height="1"&gt;</description><category domain="http://blogs.msdn.com/cjacks/archive/tags/User+Experience/default.aspx">User Experience</category></item><item><title>Creating Text Labels With a Drop Shadow Effect in Windows Forms</title><link>http://blogs.msdn.com/cjacks/archive/2006/05/26/creating-text-labels-with-a-drop-shadow-effect-in-windows-forms.aspx</link><pubDate>Sat, 27 May 2006 00:16:45 GMT</pubDate><guid isPermaLink="false">91d46819-8472-40ad-a661-2c78acb4018c:608357</guid><dc:creator>Chris Jackson</dc:creator><slash:comments>3</slash:comments><comments>http://blogs.msdn.com/cjacks/comments/608357.aspx</comments><wfw:commentRss>http://blogs.msdn.com/cjacks/commentrss.aspx?PostID=608357</wfw:commentRss><wfw:comment>http://blogs.msdn.com/cjacks/rsscomments.aspx?PostID=608357</wfw:comment><description>&lt;p&gt;One great new feature in WinFX / Windows Presentation Foundation (Avalon) is the ability to render true drop shadows using the &lt;a href="http://windowssdk.msdn.microsoft.com/library/default.asp?url=/library/en-us/cpref33/html/P_System_Windows_Media_Effects_DropShadowBitmapEffect_Softness.asp"&gt;DropShadowBitmapEffect class&lt;/a&gt;. What do I mean by true shadows? It's relatively straightforward to simply render text twice onto a form, but in the real world the edges of shadows do not have hard edges. In the real world, light does not come from a single one dimensional point in three dimensional space. The consequence is that shadows have both an umbra and a penumbra - a darker center, surrounded by a gradient from dark to light at the edges. You get this out of the box with Windows Presentation Foundation. However, even though Windows Presentation Foundation now has a &lt;a href="http://msdn.microsoft.com/winfx/downloads/products/golive/resources/default.aspx"&gt;Go Live license&lt;/a&gt;, not everyone can take a dependency on a beta framework. What I wanted to do was create such an effect using Windows Forms.&lt;/p&gt; &lt;p&gt;Given that we can understand the basic premise (darker middle, surrounded by a gradient from dark to light), we could conceivably render this manually. We could conceivably work with GraphicsPath objects to vectorize fonts, create two paths (one representing the outer edge of the penumbra and the other representing the inner edge of the penumbra / outer edge of the umbra, use a PathGradientBrush for the penumbra, and use a SolidBrush for the umbra. But that sure sounds like a lot of work, both for me and for the graphics subsystem of my trusty Tablet PC. (Using a PathGradientBrush is a fairly expensive operation.)&lt;/p&gt; &lt;p&gt;So my next thought, of course, is ... how can I cheat?&lt;/p&gt; &lt;p&gt;If you were following &lt;a href="http://blogs.msdn.com/cjacks/archive/2006/04/03/567592.aspx"&gt;my previous article on rasterizing images&lt;/a&gt;, we looked at several algorithmic options for scaling raster images. If you played around with any of these algorithms, you will begin to notice some of the blurring effects when scaling up images, and in particular if you use the HighQualityBicubic interpolation mode you end up with a nice fuzzy blur.&lt;/p&gt; &lt;p&gt;A nice fuzzy blur.&lt;/p&gt; &lt;p&gt;Around a solid colored center.&lt;/p&gt; &lt;p&gt;Hmm...&lt;/p&gt; &lt;p&gt;That sounds a little bit like what we actually want. Perhaps what is normally a bad thing when scaling up images can be put to productive use in this scenario.&lt;/p&gt; &lt;p&gt;One option for creating more realistic shadows using Windows Forms is to scale down the text that you want when you rasterize it, and then scale it back up using the HighQualityBicubic interpolation mode. Let's take a look at how that looks.&lt;/p&gt; &lt;p align="center"&gt;&lt;img src="http://blogs.msdn.com/photos/cjacks/images/608320/original.aspx"&gt;&lt;/p&gt; &lt;p align="center"&gt;&lt;cite&gt;More Realistic Text Shadows in Windows Forms&lt;/cite&gt;&lt;/p&gt; &lt;p&gt;To achieve this, I derived from the Label class. I added a few properties to describe the shadow, and then I overrode the OnPaint method. When I paint, I create a bitmap that is smaller (scaled based on the Softness property), and then apply a matrix transformation to the graphics object for this bitmap. The matrix transformation includes both scaling (so the font appears at the same relative size when scaled back up) and offset based on the Direction and ShadowDepth properties. After I have rendered onto the bitmap, I render the bitmap onto the label itself, and finally render the text directly onto the Label. The result? A more realistic shadow.&lt;/p&gt; &lt;p&gt;Is this a perfect approach? Far from it. If you manipulate the properties for a while, you will notice that there are sometimes challenges in getting the text to appear at exactly the same size. We expect this. Why? We are rasterizing the same vector two different times. The rasterizer has to determine which pixels are the best fit for the vector at two completely different scales, and you will end up with different rounding errors at each.&lt;/p&gt; &lt;p&gt;Another shortcoming is that Windows Forms will not allow the Label object to render pixels outside of its bounds. As you ratchet up the ShadowDepth property, the bitmap you render will drift further and further off the edge of the Label object's bounds, initially being cut off, and eventually disappearing entirely.&lt;/p&gt; &lt;p&gt;Nonetheless, this is an interesting cheat for obtaining a more realistic effect, and something that is kind of fun to play around with.&lt;/p&gt; &lt;p&gt;Here is the code for my MoreRealisticShadowLabel class:&lt;/p&gt;&lt;pre&gt;namespace SoftShadowLabel {

  using System;
  using System.ComponentModel;
  using System.Drawing;
  using System.Drawing.Drawing2D;
  using System.Drawing.Imaging;
  using System.Drawing.Text;
  using System.Windows.Forms;


  class MoreRealisticShadowLabel : Label {

    private Color color;
    private int direction;
    private float softness;
    private int opacity;
    private int shadowDepth;

    public MoreRealisticShadowLabel()
      : base() {
      color = Color.Black;
      direction = 315;
      softness = 2f;
      opacity = 100;
      shadowDepth = 4;
    }

    [Category("Appearance")]
    [Description("Gets or sets the color of the shadow")]
    [DefaultValue(typeof(Color), "0x000000")]
    public Color Color {
      get {
        return color;
      }
      set {
        color = value;
        Invalidate();
      }
    }

    [Category("Appearance")]
    [Description("Gets or sets the degree of opacity of the shadow")]
    [DefaultValue(100)]
    public int Opacity {
      get {
        return opacity;
      }
      set {
        if (value &amp;lt; 0 || value &amp;gt; 255) {
          throw new ArgumentOutOfRangeException("Opacity",
            "Opacity must be between 0 and 255");
        }
        opacity = value;
        Invalidate();
      }
    }

    [Category("Appearance")]
    [Description("Gets or sets how soft the shadow is")]
    [DefaultValue(2f)]
    public float Softness {
      get {
        return softness;
      }
      set {
        if (softness &amp;lt;= 0) {
          throw new ArgumentOutOfRangeException("Softness",
            "Softness must be greater than 0");
        }
        softness = value;
        Invalidate();
      }
    }

    [Category("Appearance")]
    [Description("Gets or sets the angle the shadow is cast")]
    [DefaultValue(315)]
    public int Direction {
      get {
        return direction;
      }
      set {
        if (value &amp;lt; 0 || value &amp;gt; 360) {
          throw new ArgumentOutOfRangeException("Direction", 
            "Direction must be between 0 and 360");
        }
        direction = value;
        Invalidate();
      }
    }

    [Category("Appearance")]
    [Description("Gets or sets the distance between the plane " +
      "of the object casting the shadow and the shadow plane")]
    [DefaultValue(4)]
    public int ShadowDepth {
      get {
        return shadowDepth;
      }
      set {
        if (value &amp;lt; 0) {
          throw new ArgumentOutOfRangeException("ShadowDepth",
            "ShadowDepth must be greater than 0");
        }
        shadowDepth = value;
        Invalidate();
      }
    }

    protected override void OnPaint(PaintEventArgs e) {

      Graphics screenGraphics = e.Graphics;
      Bitmap shadowBitmap = new Bitmap(Math.Max((int)(Width / softness), 1),
        Math.Max((int)(Height / softness), 1));
      using (Graphics imageGraphics = Graphics.FromImage(shadowBitmap)) {
        imageGraphics.TextRenderingHint = TextRenderingHint.AntiAliasGridFit;
        Matrix transformMatrix = new Matrix();
        transformMatrix.Scale(1 / softness, 1 / softness);
        transformMatrix.Translate((float)(shadowDepth * Math.Cos(direction)),
          (float)(shadowDepth * Math.Sin(direction)));
        imageGraphics.Transform = transformMatrix;
        imageGraphics.DrawString(Text, Font,
          new SolidBrush(Color.FromArgb(opacity, color)), 0, 0,
          StringFormat.GenericTypographic);
      }
      screenGraphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
      screenGraphics.DrawImage(shadowBitmap, ClientRectangle, 0, 0, 
        shadowBitmap.Width, shadowBitmap.Height, GraphicsUnit.Pixel);
      screenGraphics.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
      screenGraphics.DrawString(Text, Font, new SolidBrush(ForeColor), 0, 0, 
        StringFormat.GenericTypographic);
  
    }

  }
}
&lt;/pre&gt;
&lt;p&gt;You may have noticed that I am using ClearType for the text rendering, but AntiAliasGridFit for the shadow rendering. If I turn on ClearType when rendering the shadowed text to the bitmap, then things really start to go haywire, and I lose transparency altogether.&lt;/p&gt;
&lt;p&gt;The same principals can be applied to any vector drawing - this technique is not specific to text. You could also apply this technique to a raster image that includes transparency, although you would have to touch each of the pixels to change the color to your shadow color. With both, you also have to determine if you want to have your shadow alpha vary with your object alpha. Here, we assumed a constant alpha value for our text vectors, although this may not necessarily be a safe assumption.&lt;/p&gt;
&lt;p&gt;Let's see if I can talk about something other than rendering text next time around...&lt;/p&gt;&lt;img src="http://blogs.msdn.com/aggbug.aspx?PostID=608357" width="1" height="1"&gt;</description><category domain="http://blogs.msdn.com/cjacks/archive/tags/User+Experience/default.aspx">User Experience</category></item><item><title>GDI vs. GDI+ Text Rendering Performance</title><link>http://blogs.msdn.com/cjacks/archive/2006/05/19/gdi-vs-gdi-text-rendering-performance.aspx</link><pubDate>Fri, 19 May 2006 21:10:00 GMT</pubDate><guid isPermaLink="false">91d46819-8472-40ad-a661-2c78acb4018c:602021</guid><dc:creator>Chris Jackson</dc:creator><slash:comments>9</slash:comments><comments>http://blogs.msdn.com/cjacks/comments/602021.aspx</comments><wfw:commentRss>http://blogs.msdn.com/cjacks/commentrss.aspx?PostID=602021</wfw:commentRss><wfw:comment>http://blogs.msdn.com/cjacks/rsscomments.aspx?PostID=602021</wfw:comment><description>&lt;P&gt;In &lt;A href="http://blogs.msdn.com/cjacks/archive/2006/05/11/595525.aspx" mce_href="http://blogs.msdn.com/cjacks/archive/2006/05/11/595525.aspx"&gt;my last post&lt;/A&gt;, I included the unverified claim, "I have heard (but I have not personally verified) that GDI font rendering is approximately 10x faster than GDI+ font rendering (1M glyphs / second vs. 100K glyphs / second). Of course, there is a reason why GDI+ takes more work - text is truly device independent using GDI+. Yet another tradeoff. If you are interested in the details, check out &lt;A href="http://windowsforms.net/articles/gdiptext.aspx" mce_href="http://windowsforms.net/articles/gdiptext.aspx"&gt;GDI+ Text, Resolution Independence, and Rendering Methods. Or - Why does my text look different in GDI+ and in GDI?&lt;/A&gt;." Well, as you can probably guess by the title of this entry, making that unverified claim kept gnawing away at me, so when I finally had some time to cut some code, I decided to investigate that myself. What really is the performance overhead of these features?&lt;/P&gt;
&lt;P&gt;I didn't really have the time to put together a scientific study, but I was OK with that. What I really wanted was a gut check, and if I was correct within an order of magnitude, then this was good enough for me. Rendering performance is, of course, going to change based on font, character, how the characters are put together, and how we leverage any internal optimizations of the framework. To keep things simple, I decided to stick with my favorite font, Segoe UI, using ClearType. Rather than trying to select a single character, or limiting myself to letters of the alphabet, I iterated through ASCII codes from 33 to 126, all of which are printable characters. Rather than constructing arbitrary strings, I just rendered one character at a time. I wanted to render a lot of characters, but I didn't want any potential confounds from overlapping characters, so I spaced them out evenly across the entire screen and maximized my window. I wanted to take a reasonable number of samples, so I handled a mouse click to fire up a timer and invalidated the window 100 times for each rendering type. Finally, I wanted to create this in unmanaged code, for a couple of reasons. First, it eliminates another potential confound. Second, every time I create software in unmanaged code, it reminds me of just how much I prefer using managed code!&lt;/P&gt;
&lt;P align=center&gt;&lt;IMG src="http://blogs.msdn.com/photos/cjacks/images/601961/original.aspx" mce_src="http://blogs.msdn.com/photos/cjacks/images/601961/original.aspx"&gt;&lt;/P&gt;
&lt;P align=center&gt;&lt;CITE&gt;Simple Application for Measuring GDI and GDI+ Font Rendering Performance&lt;/CITE&gt;&lt;/P&gt;
&lt;P&gt;What did I discover? On my Toshiba M200 Tablet PC, using a GeForce FX 5200Go with 32 MB of graphics memory, the absolute values of my numbers were lower. However, the relative values did align with the claims that I published earlier: GDI+ font rendering was approximately an order of magnitude slower than GDI rendering, proving once again that executing code is more expensive than not executing code. Specifically, my GDI code path was rendering approximately 99,000 glyphs per second, while my GDI+ code path was rendering approximately 16,000 glyphs per second.&lt;/P&gt;
&lt;P&gt;Of course, I have rendered my share of text using GDI+ and it was always fast enough that I never thought twice about sacrificing the important features and looking for something faster, but it's always good to know what the options and tradeoffs are. This is something to think long and hard about. There is a big difference between optimizing away waste and optimizing away features. If I have a computer that is much more powerful than what was available when GDI was designed, I could theoretically just run the same applications much, much faster. Alternately, I could run updated versions of those new applications that provide me with tangible (whether or not they are particularly sexy) benefits. Resolution independence is a good thing. Enhanced readability is a good thing. Only when I find an audience who is reading more than 16,000 glyphs per second (some serious speed readers) am I going to start wanting to thinking about the trade-offs. With the implementation I have here, I get noticeably more flicker with the GDI+ implementation than I do with the GDI implementation, but double buffering is a solution I would consider before losing the ability to satisfy a user 2 years from now using a 500 dpi screen who thinks my font rendering is less readable than competing applications.&lt;/P&gt;
&lt;P&gt;For those who are interested, this is the code I used for my little experiment. It's quick and dirty, but it was good enough for my limited purposes.&lt;/P&gt;&lt;PRE&gt;#include &amp;lt;WINDOWS.H&amp;gt;
#include &amp;lt;STRSAFE.H&amp;gt;
#include &amp;lt;GDIPLUS.H&amp;gt;

#define ID_TIMER 1

using namespace Gdiplus;

LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, 
                    LPSTR lpCmdLine, int nShowCmd) {

  HWND hwnd;
  MSG msg;
  WNDCLASS wndclass;
  GdiplusStartupInput gdiplusStartupInput;
  ULONG_PTR gdiplusToken;
  static TCHAR szAppName[] = TEXT("GDI vs. GDI+ Performance");

  GdiplusStartup(&amp;amp;gdiplusToken, &amp;amp;gdiplusStartupInput, NULL);

  wndclass.style = CS_HREDRAW | CS_VREDRAW;
  wndclass.lpfnWndProc = WndProc;
  wndclass.cbClsExtra = 0;
  wndclass.cbWndExtra = 0;
  wndclass.hInstance = hInstance;
  wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
  wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
  wndclass.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH);
  wndclass.lpszMenuName = NULL;
  wndclass.lpszClassName = szAppName;

  RegisterClass(&amp;amp;wndclass);

  hwnd = CreateWindow(szAppName, szAppName, 
    WS_OVERLAPPEDWINDOW | WS_MAXIMIZE, CW_USEDEFAULT, 
    CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL,
    NULL, hInstance, NULL);
  ShowWindow(hwnd, nShowCmd);
  UpdateWindow(hwnd);

  while (GetMessage(&amp;amp;msg, NULL, 0, 0)) {
    TranslateMessage(&amp;amp;msg);
    DispatchMessage(&amp;amp;msg);
  }

  GdiplusShutdown(gdiplusToken);

  return msg.wParam;
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, 
                         WPARAM wParam, LPARAM lParam) {

  static LOGFONT lf;
  static double gdiDuration = 0;
  static double gdiplusDuration = 0;
  static __int64 gdiGlyphCount = 0;
  static __int64 gdiplusGlyphCount = 0;
  static int timerCounter;
  static bool useGdi = true;

  switch (message) {

    case WM_CREATE:
      lf.lfWidth = 0;
      lf.lfItalic = FALSE;
      lf.lfQuality = 6; //CLEARTYPE_NATURAL_QUALITY
      StringCchCopy(lf.lfFaceName, 9, TEXT("Segoe UI"));
      return 0;

    case WM_LBUTTONDOWN:
      timerCounter = 0;
      gdiDuration = 0;
      gdiplusDuration = 0;
      gdiGlyphCount = 0;
      gdiplusGlyphCount = 0;
      SetTimer(hwnd, ID_TIMER, 100, NULL);
      return 0;

    case WM_TIMER:
      if (timerCounter++ &amp;lt; 100) {
        useGdi = true;
        InvalidateRect(hwnd, NULL, TRUE);
      } else if (timerCounter &amp;lt; 200) {
        useGdi = false;
        InvalidateRect(hwnd, NULL, TRUE);
      } else {
        KillTimer(hwnd, ID_TIMER);
        __int64 frequency;
        QueryPerformanceFrequency((LARGE_INTEGER*)&amp;amp;frequency);
        gdiDuration = (double)gdiGlyphCount /
          (gdiDuration / (double)frequency);
        gdiplusDuration = (double)gdiplusGlyphCount /
          (gdiplusDuration / (double)frequency);
        wchar_t durationString[100];
        swprintf_s(durationString, 100, 
          TEXT("GDI:\t%0.5f glyphs/sec\nGDI+:\t%0.5f glyphs/sec"),
          gdiDuration, gdiplusDuration);
        MessageBox(hwnd, durationString, TEXT("Duration"), 0);
      }
      return 0;

    case WM_PAINT:

      {

        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(hwnd, &amp;amp;ps);
        RECT clientRect;
        GetClientRect(hwnd, &amp;amp;clientRect);

        lf.lfHeight = -MulDiv(12, GetDeviceCaps(hdc, LOGPIXELSY), 72);
        SelectObject(hdc, CreateFontIndirect(&amp;amp;lf));
        TEXTMETRIC tm;
        GetTextMetrics(hdc, &amp;amp;tm);
        RECT charRect;
        charRect.top = 0;
        charRect.bottom = tm.tmHeight;
        char displayChar = 33;

        Graphics g(hdc);
        g.SetTextRenderingHint(TextRenderingHintClearTypeGridFit);
        SolidBrush brush(Color(255, 0, 0, 0));
        FontFamily fontFamily(L"Segoe UI");
        Font font(&amp;amp;fontFamily, 12, FontStyleRegular, UnitPoint);

        __int64 start;
        __int64 end;
        QueryPerformanceCounter((LARGE_INTEGER*)&amp;amp;start);

        while (charRect.bottom &amp;lt; clientRect.bottom) {
          charRect.left = 0;
          charRect.right = tm.tmMaxCharWidth;
          while (charRect.right &amp;lt; clientRect.right) {
            PointF pointF(charRect.left, charRect.top);
            WCHAR charString[2];
            charString[0] = (WCHAR)displayChar;
            charString[1] = '\0';
            if (useGdi) {
              DrawText(hdc, charString, -1, &amp;amp;charRect, DT_SINGLELINE);
              gdiGlyphCount++;
            } else {
              g.DrawString(charString, -1, &amp;amp;font, pointF, &amp;amp;brush);
              gdiplusGlyphCount++;
            }
            displayChar = displayChar &amp;lt; 126 ? displayChar + 1 : 33;
            charRect.left += tm.tmMaxCharWidth;
            charRect.right += tm.tmMaxCharWidth;
          }
          charRect.top += tm.tmHeight;
          charRect.bottom += tm.tmHeight;
        }
        QueryPerformanceCounter((LARGE_INTEGER*)&amp;amp;end);
        if (useGdi) {
          gdiDuration += (double)end - (double)start;
        } else {
          gdiplusDuration += (double)end - (double)start;
        }

        DeleteObject(SelectObject(hdc, GetStockObject(SYSTEM_FONT)));
        EndPaint(hwnd, &amp;amp;ps);
        return 0;

      }

    case WM_DESTROY:
      PostQuitMessage(0);
      return 0;
  }
  return DefWindowProc(hwnd, message, wParam, lParam);
}
&lt;/PRE&gt;
&lt;P&gt;You may notice one thing about how I am timing: while I have moved a lot of the code outside of the timer, there is still quite a bit inside. This is another tradeoff that I had to make in measurement. If I just timed the rendering calls themselves, then there is a larger percentage of error incorporated into my measurements. Moved outside, as it is now, I gain precision, while at the same time losing it by incorporating non-rendering code in my measurement. Neither option seemed perfect, but the latter seemed the superior approach to me. I did take the time to measure it both ways, however - the outcome with timing each individual rendering call was slightly lower performance numbers: approximately 89,000 glyphs/second with GDI and approximately 10,000 glyphs/second with GDI+. However, the relationship between the two was very similar, so my confidence was not shaken.&lt;/P&gt;
&lt;P&gt;Of course, if you are interested at all in this line of thought, you may also be interested in rendering performance using Windows Presentation Foundation ("Avalon"). However, that is someplace I am not willing to go at this point. First of all, it would be completely unfair to start benchmarking beta code. I certainy wouldn't want to benchmark beta code on a beta operating system running on beta WDDM drivers! Furthermore, as DirectX 10 hardware acceleration begins to arrive in consumer products, this will change dramatically. I remember back during the Windows XP (Whistler) beta the remarkable difference we experienced in some of the new 2D effects (such as the "fade to grey" effect when you are logging out) once video cards and drivers began to support 2D hardware acceleration...&lt;/P&gt;&lt;img src="http://blogs.msdn.com/aggbug.aspx?PostID=602021" width="1" height="1"&gt;</description><category domain="http://blogs.msdn.com/cjacks/archive/tags/User+Experience/default.aspx">User Experience</category></item><item><title>Windows Forms Font Hinting with TextRenderingHint</title><link>http://blogs.msdn.com/cjacks/archive/2006/05/11/595525.aspx</link><pubDate>Thu, 11 May 2006 20:41:37 GMT</pubDate><guid isPermaLink="false">91d46819-8472-40ad-a661-2c78acb4018c:595525</guid><dc:creator>Chris Jackson</dc:creator><slash:comments>7</slash:comments><comments>http://blogs.msdn.com/cjacks/comments/595525.aspx</comments><wfw:commentRss>http://blogs.msdn.com/cjacks/commentrss.aspx?PostID=595525</wfw:commentRss><wfw:comment>http://blogs.msdn.com/cjacks/rsscomments.aspx?PostID=595525</wfw:comment><description>&lt;p&gt;After poking around the concepts of raster and vector images, I really wanted to dive a bit deeper into the most common form of vector image you will find today: fonts. Most of the fonts you run into today on a Windows system will be either True Type or Open type fonts, which are vector (outline) fonts. And, just like vector images you draw yourself (or metafiles that contain instructions to draw vector images), fonts have to be rasterized before you can display them on a screen. (Unless, of course, you procured your monitor from an old Asteroids video game machine, in which case it can render true vectors.) Rasterizing fonts has the same considerations as rasterizing any other vector images - when you map an infinitely precise mathematical line segment against a discrete pixel, you need software to determine what each pixel should display. With most vector images, it uses a generic algorithm. The results you obtain using this same generic algorithm against fonts can be disappointing, to say the least.&lt;/p&gt;
&lt;p&gt;When you render text using Windows forms, you can specify the Text Rendering Hint on the Graphics object.  I created a short program to iterate through the various options. This is the code that I used:&lt;/p&gt;
&lt;pre&gt;
protected override void OnPaint(PaintEventArgs e) {
  Graphics graphics = e.Graphics;
  string text = "the quick brown fox jumped over the lazy dog";
  SizeF size = graphics.MeasureString(text, Font);
  using (SolidBrush brush = new SolidBrush(ForeColor)) {
    for (int i = 0; i &lt; 6; i++) {
      graphics.TextRenderingHint = TextRenderingHint.SystemDefault;
      graphics.DrawString(((TextRenderingHint)i).ToString(), Font, brush, 
        new PointF(0, size.Height * i));
      graphics.TextRenderingHint = (TextRenderingHint)i;
      graphics.DrawString(text, Font, brush, new PointF(125, size.Height * i));
    }
  }
}
&lt;/pre&gt;
&lt;p&gt;Let's take a look at what we come up with. For this discussion, I am not interested in the differences between various anti-aliasing options, such as clear type. Personally, I'm fanatical about turning on ClearType everwhere that I can, but that is beside the point. The differences I am interested in looking at are between the GridFit enumeration values and the rest. (You can think of the GridFit suffix as basically means to use font hinting. However, this isn't entirely true. For grid fitting, the font glyph outlines are actually being moved around to best fit the bitmap grid On top of that, font hinting will be used to make additional suggestions to the rasterizer as to how the glyph should be rendered.)&lt;/p&gt;
&lt;p align="center"&gt;&lt;img src="http://blogs.msdn.com/photos/cjacks/images/595434/original.aspx" /&gt;&lt;/p&gt;
&lt;p align="center"&gt;&lt;cite&gt;8-Point Segoe UI with Various Text Rendering Hints&lt;/cite&gt;&lt;/p&gt;
&lt;cite&gt;As an aside, I am a big fan of the Segoe UI font, which is the system font for Windows Vista and is also the font used by the 2007 Office System. Curiously, while a variant on this font is used for Windows Mobile 5.0 Smartphones, it is not used for Windows Mobile 5.0 Pocket PCs. And, unfortunately, the Palm Treo 700w I use is built on the Pocket PC version. I have absolutely no idea why I am stuck with Tahoma on my phone, which I find much less pleasurable to use. Bummer.&lt;/cite&gt;
&lt;p&gt;The differences are much more obvious on smaller fonts, which is why I selected an 8-point font. If you zero in on the SingleBitPerPixel rendering, you will notice that some of the characters aren't even complete! This is where font hinting helps out. It is a way that the developer of the font can make explicit suggestions to improve rendering.&lt;/p&gt;
&lt;p&gt;Why might you care about this? Well, I found a few useful nuggets out of this little exploration.&lt;/p&gt;
&lt;p&gt;First of all, if you are concerned about performance, it's good to know what your costs and trade-offs are. Rendering fonts includes rasterization if you are using an outline font. If you turn on hinting, then you have to actually run code (and yes, hinting code can contain loops) above and beyond rasterization code. But, from a trade-off perspective, it's very clear to me that these are CPU cycles that are very worthwhile! If you use bitmapped fonts (and there still are some in a standard installation of Windows) then you do not have to pay either cost. However, you also can't take advantage of anti-aliasing - either standard or clear type. (Personally, I can't stand to use software that uses bitmapped fonts any more. If it doesn't support clear type, and I am not compelled to use it for some reason, I'll remove the program.)&lt;/p&gt;
&lt;p&gt;Notice that there is an option for ClearType. Normally, you may think of ClearType as a system-wide setting, but its presence here should suggest that you can render fonts using Clear Type even if this smoothing technique has not been enabled system-wide! While normally you would generally prefer to honor the user's system settings, with a reading technology as powerful as Clear Type that wasn't enabled by default when Windows XP was released (and where the setting is potentially non-obvious to somebody who isn't a power user) it is nice to know that you could provide this benefit to your user base without requiring them to discover this setting.&lt;/p&gt;
&lt;p&gt;You probably noticed that the text we are outputting are not all exactly the same length. The GridFit fonts tend to be longer. This isn't entirely unexpected - if we are moving glyphs around to find the best fit for the bitmap grid we are rasterizing to, you probably expect that the movement will more often be to the right (on right to left text) so that you don't end up with overlap and crowding with the previous glyph. However, if you inject some code to use the Graphics.MeasureString API, you will discover that the length of the string is reported as always being the same, even though your eyes tell you differently! This is actually a more complex operation than it seems. To measure the size of a string, are doing a translation from a vector image to a raster image. This translation also includes grid fitting and hinting. Apparently, the MeasureString API is not running all of this code before returning a response, and the most obvious reason why is that it does this in the interest of performance. Considering trade-offs, the question then becomes this - how important is it to be to-the-pixel accurate, compared to the value of the CPU cycles that could otherwise be doing something the user could actually see. For example, I am using the MeasureString API to find an approximation of the string's height in order to space out my output. If the result is 1 pixel greater than it would otherwise need to be, do I really care? In this example, I do not. However, it's good to keep this in mind when you use this API.&lt;/p&gt;
&lt;p&gt;You will also notice that I used the Graphics.DrawString API rather than the TextRender.MeasureText API. TextRenderer internally uses some font caching, as well as calling into the GDI function DrawTextEx. The result is that everything I render is rendered using hinted fonts, which made it a poor choice for the purpose of this demo. Wait ... it's using GDI instead of GDI+? I have heard (but I have not personally verified) that GDI font rendering is approximately 10x faster than GDI+ font rendering (1M glyphs / second vs. 100K glyphs / second). Of course, there is a reason why GDI+ takes more work - text is truly device independent using GDI+. Yet another tradeoff. If you are interested in the details, check out &lt;a href="http://windowsforms.net/articles/gdiptext.aspx"&gt;GDI+ Text, Resolution Independence, and Rendering Methods. Or - Why does my text look different in GDI+ and in GDI?&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Knowing about font hinting is also important if you are using text with GraphicsPath objects. The GraphicsPath object exposes AddString methods, and you can use this to include text in your path. However, internally, a GraphicsPath object contains nothing but a collection of points. There are no exceptions made to store text. That means that the text is stored as the points required to construct this image. Once the text is converted to points, there is no way to differentiate these points from any other points in the path. And this, of course, means that when you rasterize this path, you will be rasterizing the points comprising the text, leveraging none of the grid fitting and hinting provided by fonts. If your fonts are smaller, the outcome could likely be as unattractive and unreadable as the SingleBitPerPixel font rendering.&lt;/p&gt;
&lt;p&gt;The final thing I pondered was this: if the technique of hinting so visibly helpful in rendering font vectors, wouldn't it be just as helpful in rendering arbitrary vectors? I certainly think so. Obviously, the completeness of a font has a much higher impact than the completeness of an arbitrary picture - you want to be able to process a large number of glyphs as efficiently as possible. However, as we slowly (glacially) make our way towards true device independence and vector images, I can foresee a need to support vector hinting on arbitrary vector images, and not fonts alone. I have no idea if or when this is coming...&lt;/p&gt;
&lt;p&gt;Note that I am not an expert in the inner workings of fonts, and that I have discovered many of these things during the course of my exploration. I just need to use them. If you have something to add or correct, please feel free to comment, trackback, or email!&lt;/p&gt;&lt;img src="http://blogs.msdn.com/aggbug.aspx?PostID=595525" width="1" height="1"&gt;</description><category domain="http://blogs.msdn.com/cjacks/archive/tags/User+Experience/default.aspx">User Experience</category></item><item><title>Manipulating HSB (HSL) Values to Create Windows Forms Panel Background Colors</title><link>http://blogs.msdn.com/cjacks/archive/2006/04/28/586250.aspx</link><pubDate>Fri, 28 Apr 2006 22:32:40 GMT</pubDate><guid isPermaLink="false">91d46819-8472-40ad-a661-2c78acb4018c:586250</guid><dc:creator>Chris Jackson</dc:creator><slash:comments>1</slash:comments><comments>http://blogs.msdn.com/cjacks/comments/586250.aspx</comments><wfw:commentRss>http://blogs.msdn.com/cjacks/commentrss.aspx?PostID=586250</wfw:commentRss><wfw:comment>http://blogs.msdn.com/cjacks/rsscomments.aspx?PostID=586250</wfw:comment><description>&lt;p&gt;&lt;a href="http://blogs.msdn.com/cjacks/archive/2006/04/12/575476.aspx"&gt;In my last entry&lt;/a&gt;, I provided some code to convert from the HSB color space to the RGB color space.&lt;/p&gt;
&lt;cite&gt;Note that we are actually referring to the HSL color space, not the HSB color space. I am using the name HSB because this is the convention used by the .NET Framework and its supporting documentation.&lt;/cite&gt;
&lt;p&gt;Now, the time has come to make this code come to life, rather than being interesting only to those who are interested in math or color spaces!&lt;/p&gt;
&lt;p&gt;To explore some of the possibilities of using the HSL color space (which maps more intuitively to the human perception of color), I created a class deriving from Panel, and used the background color to generate 4 different colors with the same hue, but different values for saturation and brightness (luminance), which I then used to creating an interesting looking gradient.&lt;/p&gt;
&lt;p align="center"&gt;&lt;img src="http://blogs.msdn.com/photos/cjacks/images/586196/original.aspx" /&gt;&lt;/p&gt;
&lt;p align="center"&gt;&lt;cite&gt;Custom Gradient Panel Based On a Single Color's Hue&lt;/cite&gt;&lt;/p&gt;
&lt;p&gt;This highlights some of the interesting possibilities of mapping color to something that humans feel more comfortable manipulating. Starting with a single color, I can now generate a gradient, and I can generate it for all possible colors. This might be useful if we wanted to create an interesting looking toolbar (shown at the top), or interesting looking headings (shown in the body) without having to specify specific colors for each endpoint of the gradient.&lt;/p&gt;
&lt;p&gt;The top panel (the one emulating a toolbar) is based on a System Color - Active Caption. Using this technique, we are now generating a 4 color gradient based on the active caption color - in this case blue. If I switch the color scheme to the olive green one, now my color is based on olive green. Handy! However, we'll come back to this one in a second. The header panel I have labelled Group 1 is based on the color Red. The header panel I have labeled Group 2 is based on the system color Control.&lt;/p&gt;
&lt;p&gt;Wait a minute ... isn't the Control system color grey, and not in any way electric purple? That is one thing to watch out for if you attempt to base the colors on external colors that are outside of your control. Note that we are modifying both saturation and brightness (luminance) to come up with our gradient. In the HSL color space, there is no hue for grey at all - grey is simply a color (any color you want) with a saturation of 0. Hues of 0 through 360 with a saturation of 0 and a brightness (luminance) indicating the shade of grey are all exactly identical. So, when we take a shade of grey, we can never be entirely sure which specific hue will have been used, since it does not matter in the least. As such, it is probably best to avoid using this technique and tying it to a color over which you have no control, because shades of grey break the model in this color space. You will notice the same thing in the panel I am using as a toolbar if you change your system colors to the silver color scheme: instead of a grey gradient, you end up with a red gradient!&lt;/p&gt;
&lt;p&gt;Of course, we could take this into account and more intelligently modify saturation and lightness, rather than just hard coding explicit values as we have done here. This is left as an exercise for the reader.&lt;/p&gt;
&lt;p&gt;With that said, let's take a look at the code:&lt;/p&gt;
&lt;pre&gt;
class FourColorPanel : Panel {

    private Color topTopColor;
    private Color topBottomColor;
    private Color bottomTopColor;
    private Color bottomBottomColor;

    public FourColorPanel() : base() {
        SetBackgroundColors(BackColor.GetHue());
    }

    public override System.Drawing.Color BackColor {
        get {
            return base.BackColor;
        }
        set {
            base.BackColor = value;
            SetBackgroundColors(value.GetHue());
        }
    }

    private void SetBackgroundColors(float baseHue) {
        topTopColor = ColorConversions.ColorFromAhsb(255, baseHue,
            0.2958f, 0.7292f);
        topBottomColor = ColorConversions.ColorFromAhsb(255, baseHue,
            0.5875f, 0.35f);
        bottomTopColor = ColorConversions.ColorFromAhsb(255, baseHue,
            0.7458f, 0.2f);
        bottomBottomColor = ColorConversions.ColorFromAhsb(255, baseHue,
            0.6f, 0.4042f);
    }

    protected override void OnPaint(PaintEventArgs e) {
        Rectangle topRect = new Rectangle(0, 0, ClientRectangle.Width,
            ClientRectangle.Height / 2);
        Rectangle bottomRect = new Rectangle(0, topRect.Height,
            ClientRectangle.Width,
            ClientRectangle.Height - topRect.Height - 1);
        using (Brush topBrush = new LinearGradientBrush(topRect,
            topTopColor, topBottomColor, LinearGradientMode.Vertical)) {
            e.Graphics.FillRectangle(topBrush, topRect);
        }
        using (Brush bottomBrush = new LinearGradientBrush(bottomRect,
            bottomTopColor, bottomBottomColor,
            LinearGradientMode.Vertical)) {
            e.Graphics.FillRectangle(bottomBrush, bottomRect);
        }
    }

}
&lt;/pre&gt;
&lt;p&gt;Hopefully, this technique can open up new doors for creating interesting color combinations based on a single hue, rather than requiring that every single color be explicitly specified.&lt;/p&gt;&lt;img src="http://blogs.msdn.com/aggbug.aspx?PostID=586250" width="1" height="1"&gt;</description><category domain="http://blogs.msdn.com/cjacks/archive/tags/User+Experience/default.aspx">User Experience</category></item><item><title>Converting from HSB to RGB in .NET</title><link>http://blogs.msdn.com/cjacks/archive/2006/04/12/575476.aspx</link><pubDate>Thu, 13 Apr 2006 05:38:33 GMT</pubDate><guid isPermaLink="false">91d46819-8472-40ad-a661-2c78acb4018c:575476</guid><dc:creator>Chris Jackson</dc:creator><slash:comments>12</slash:comments><comments>http://blogs.msdn.com/cjacks/comments/575476.aspx</comments><wfw:commentRss>http://blogs.msdn.com/cjacks/commentrss.aspx?PostID=575476</wfw:commentRss><wfw:comment>http://blogs.msdn.com/cjacks/rsscomments.aspx?PostID=575476</wfw:comment><description>&lt;p&gt;I want to digress a bit from the controls we have been drawing and measuring to talk a bit about color. In some of the examples we have been building, we have been trying to make it possible to set the color just one time rather than explicitly specifying background colors and highlight colors. Consequently, we have been doing our highlights using white, which ensures that they work equally well with any color the consuming application selects. Unless, of course, the consuming application select white, in which case it's not so great.&lt;/p&gt;
&lt;p&gt;In the last control we were building, we were explicitly hard coding two colors, and using a radial gradient fill using these two colors. Now, we are putting the impetus on the developer of the consuming application to select two colors that look good together, rather than a single one - and both of these must look good with white as the highlight color. We are making the job more and more complex for the developer consuming our controls.&lt;/p&gt;
&lt;p&gt;It would be easier if we could programatically discover related colors, wouldn't it? That way, the developer of the consuming application could input a single color, and we could intuit related colors and use them. One approach is to use the HSB color space - Hue, Saturation, and Brightness. Using this approach, we can start with one color, and then vary these properties, which seem more intuitive. What does it mean to add 20 to the blue value of a given color? We have to think about that one for a bit. What does it mean to add 20 to the brightness of some color? We can picture that much more easily.&lt;/p&gt;
&lt;p&gt;Starting with the same Hue, we could vary the brightness - creating a highlight that fits with the starting color. If we have a more advanced understanding of complementary colors, we could discover a complementary hue around the 360 degree Hue value space. The space is more easy to navigate. Plus, if you have used the Color Picker dialog, you may have already noticed that HSB (here referred to as Hue, Saturation, Lumens) is something that is widely used throughout Windows applications.&lt;/p&gt;
&lt;p align="center"&gt;&lt;img src="http://blogs.msdn.com/photos/cjacks/images/575443/original.aspx" /&gt;&lt;/p&gt;
&lt;p align="center"&gt;&lt;cite&gt;Color Picker dialog box&lt;/cite&gt;&lt;/p&gt;
&lt;p&gt;We used a special case of this scenario while creating Gel Buttons earlier, converting to grayscale by applying the brightness value to the R, G, and B channels. Now, we just need to finish our work to perform this conversion in the general case.&lt;/p&gt;
&lt;p&gt;Of course, as good developers, our first instinct is to let somebody else do that work, so we can focus our energy on solving unique problems that do not yet have a known solution. Searching around a bit, you can find that somebody has a class submitted to The Code Project to do exactly that: &lt;a href="http://www.codeproject.com/dotnet/HSBColorClass.asp"&gt;Use both RGB and HSB color schemas in your .NET application using HSBColor class&lt;/a&gt;. How convenient! Unfortunately, if you use the algorithm provided by this class, you will notice that if you take an existing Color structure, retrive the HSB values using &lt;code&gt;GetHue()&lt;/code&gt;, &lt;code&gt;GetSaturation()&lt;/code&gt;, and &lt;code&gt;GetBrightness()&lt;/code&gt;, the Color that is returned is &lt;strong&gt;not&lt;/strong&gt; the color that you began with. That's certainly not a good thing.&lt;/p&gt;
&lt;p&gt;Poking around a bit more, I discovered another site called &lt;a href="http://www.cs.rit.edu/~ncs/color/t_convert.html"&gt;Color Conversion Algorithms&lt;/a&gt; at the Rochester Institute of Technology. However, the algorithm it presents had similar problems - the HSB model it used apparently differs from the HSB model used by the .NET Framework.&lt;/p&gt;
&lt;p&gt;Since I think it is important to not overload the terms Hue, Saturation, and Brightness within a platform, it appeared as if I was going to need to create the implementation on my own. While I could have simply provided both the mechanism to convert both to and from HSB from the standard RGB values, ensuring the symmetry of the altorighms uses, I want somebody to be able to pass in the results of a call to &lt;code&gt;GetHue()&lt;/code&gt;, &lt;code&gt;GetSaturation()&lt;/code&gt;, and &lt;code&gt;GetBrightness()&lt;/code&gt; and correctly anticipate the results. So, it's time to figure out how the algorithms differ.&lt;/p&gt;
&lt;p&gt;Taking the values one by one, it is immediately apparent that Brightness is computed differently. In the algorithms used by the above implementations, the Brightness is always set to the value of the RGB color with the highest value. However, this is obviously not true of the implementation in the .NET Framework. If you plug in a few examples, you can fairly quickly determine that the Brightness is, instead, computed as (Min(R,G,B) + Max(R,G,B)) / 2. To me, this actually made more sense than simply taking the maximum value. If, for example, you used the Brighness value to convert to grayscale, the above algorithms will make pure red, pure green, and pure blue all indistinguisable from pure white. With the .NET Framework implementation, it would be half as bright, and clearly distinguishable.&lt;/p&gt;
&lt;p&gt;Saturation turns out to have a similarly more sophisticated algorithm. Hue, however, turns out to be identical, which is completely logical.&lt;/p&gt;
&lt;p&gt;Once we have computed these formula, it is simply a matter of performing some high school-level algebra to compute values going in the other direction. The result is the following algorithm, expressed in a C# implementation:&lt;/p&gt;
&lt;pre&gt;
public static Color ColorFromAhsb(int a, float h, float s, float b) {

  if (0 &gt; a || 255 &lt; a) {
    throw new ArgumentOutOfRangeException("a", a, 
      Properties.Resources.InvalidAlpha);
  }
  if (0f &gt; h || 360f &lt; h) {
    throw new ArgumentOutOfRangeException("h", h, 
      Properties.Resources.InvalidHue);
  }
  if (0f &gt; s || 1f &lt; s) {
    throw new ArgumentOutOfRangeException("s", s, 
      Properties.Resources.InvalidSaturation);
  }
  if (0f &gt; b || 1f &lt; b) {
    throw new ArgumentOutOfRangeException("b", b, 
      Properties.Resources.InvalidBrightness);
  }

  if (0 == s) {
    return Color.FromArgb(a, Convert.ToInt32(b * 255), 
      Convert.ToInt32(b * 255), Convert.ToInt32(b * 255));
  }

  float fMax, fMid, fMin;
  int iSextant, iMax, iMid, iMin;

  if (0.5 &lt; b) {
    fMax = b - (b * s) + s;
    fMin = b + (b * s) - s;
  } else {
    fMax = b + (b * s);
    fMin = b - (b * s);
  }

  iSextant = (int)Math.Floor(h / 60f);
  if (300f &lt;= h) {
    h -= 360f;
  }
  h /= 60f;
  h -= 2f * (float)Math.Floor(((iSextant + 1f) % 6f) / 2f);
  if (0 == iSextant % 2) {
    fMid = h * (fMax - fMin) + fMin;
  } else {
    fMid = fMin - h * (fMax - fMin);
  }
      
  iMax = Convert.ToInt32(fMax * 255);
  iMid = Convert.ToInt32(fMid * 255);
  iMin = Convert.ToInt32(fMin * 255);

  switch (iSextant) {
    case 1:
      return Color.FromArgb(a, iMid, iMax, iMin);
    case 2:
      return Color.FromArgb(a, iMin, iMax, iMid);
    case 3:
      return Color.FromArgb(a, iMin, iMid, iMax);
    case 4:
      return Color.FromArgb(a, iMid, iMin, iMax);
    case 5:
      return Color.FromArgb(a, iMax, iMin, iMid);
    default:
      return Color.FromArgb(a, iMax, iMid, iMin);
  }
}
&lt;/pre&gt;
&lt;p&gt;Of course, we had to do quite a bit of mathematical manipulation to arrive at this implementation. I am not sure if you are willing to rely on my mathematical acumen, but I know that I certainly am not! This calls for unit testing.&lt;/p&gt;
&lt;p&gt;This is actually an interesting bit of code to implement unit tests for. The theoretical number of combinations of HSB values you could provide is infinite. (I say theoretical because the implementation of the float data type is not infinitely precise.) So, where do we begin?&lt;/p&gt;
&lt;p&gt;One obvious starting point is to begin with each possible permutation of RGB colors. If we pass in the HSB values computed by the .NET Framework to create a new color, do we end up with the same color? We can write this test fairly easily:&lt;/p&gt;
&lt;pre&gt;
public void TestAllArgbValues() {
  for (int a = 0; a &lt;= 255; a++) {
    for (int r = 0; r &lt;= 255; r++) {
      for (int g = 0; g &lt;= 255; g++) {
        for (int b = 0; b &lt;= 255; b++) {
          Color startColor = Color.FromArgb(a, r, g, b);
          Color endColor = ColorConversions.ColorFromAhsb(startColor.A,
            startColor.GetHue(), startColor.GetSaturation(),
            startColor.GetBrightness());
          Assert.AreEqual&lt;int&gt;(startColor.A, endColor.A,
            string.Format("Alpha does not match for {0},{1},{2},{3}",
            a, r, g, b));
          Assert.AreEqual&lt;int&gt;(startColor.R, endColor.R,
            string.Format("Red does not match for {0},{1},{2},{3}",
            a, r, g, b));
          Assert.AreEqual&lt;int&gt;(startColor.G, endColor.G,
            string.Format("Green does not match for {0},{1},{2},{3}",
            a, r, g, b));
          Assert.AreEqual&lt;int&gt;(startColor.B, endColor.B,
            string.Format("Blue does not match for {0},{1},{2},{3}",
            a, r, g, b));
        }
      }
    }
  }
}
&lt;/pre&gt;
&lt;p&gt;However, it turns out that this is not such a great unit test after all. Can you spot the problem? If you said, "For the love of code, that unit test has 4,294,967,296 permutations!" you are correct. I started that unit test at 9:00 at night. The next morning, it was still running. It was only when I got home from work that day that it had completed. Fortunately, it had passed! However, we certainly don't want to spend a day running a unit test every single time, so I marked that test with the &lt;code&gt;IgnoreAttribute&lt;/code&gt; and tried again.&lt;/p&gt;
&lt;p&gt;What is a good subset of every possible permutation of RGB colors? How could you select from them? The route I took was to select all &lt;code&gt;KnownColor&lt;/code&gt; members, which should be fairly representative, not to mention disproportionally representing the colors we are likely to receive from a caller. So, the unit test I came up with for this was:&lt;/p&gt;
&lt;pre&gt;
public void TestKnownColors() {
  Array colorsArray = Enum.GetValues(typeof(KnownColor));
  KnownColor[] allColors = new KnownColor[colorsArray.Length];
  Array.Copy(colorsArray, allColors, colorsArray.Length);
  for (int i = 0; i &lt; allColors.Length; i++) {
    Color startColor = Color.FromName(allColors[i].ToString());
    Color endColor = ColorConversions.ColorFromAhsb(startColor.A, 
      startColor.GetHue(), startColor.GetSaturation(),
      startColor.GetBrightness());
    Assert.AreEqual&lt;int&gt;(startColor.A, endColor.A,
      "Alpha does not match for color " + allColors[i].ToString());
    Assert.AreEqual&lt;int&gt;(startColor.R, endColor.R, 
      "Red does not match for color " + allColors[i].ToString());
    Assert.AreEqual&lt;int&gt;(startColor.G, endColor.G, 
      "Green does not match for color " + allColors[i].ToString());
    Assert.AreEqual&lt;int&gt;(startColor.B, endColor.B, 
      "Blue does not match for color " + allColors[i].ToString());
  }
}
&lt;/pre&gt;
&lt;p&gt;And, not surprisingly, this unit test passed as well. Are we done? Well, it's possible that we could pass in HSB values that do not directly correspond to an RGB color (with is much more finite integer values). How do we know that these would be converted correctly?&lt;/p&gt;
&lt;p&gt;In order to test these permutations, we would have to come up with an operant definition for what the best Color approximation of an HSB value would be. We could potentially define this as the Color structure who's HSB values differed from the input HSB values by the smallest amount. However, then we would need to iterate over the same huge number of possible colors for each initial HSB value. Given that we have a theoretically infinite number of HSB values that do not directly correspond to a Color structure, suddently we have infinity times a huge number, with each of the infinite iterations requiring approximately a day to run. In the end, I decided that I was confident enough in my math to forego this exercise.&lt;/p&gt;
&lt;p&gt;In the end, we have created some useful new functionality, which is consistent with the computation of HSB values that the platform provides. Later, we will use this ability to start to do some interesting things.&lt;/p&gt;&lt;img src="http://blogs.msdn.com/aggbug.aspx?PostID=575476" width="1" height="1"&gt;</description><category domain="http://blogs.msdn.com/cjacks/archive/tags/User+Experience/default.aspx">User Experience</category></item><item><title>Performance Effects of Scaling Raster Images</title><link>http://blogs.msdn.com/cjacks/archive/2006/04/03/567592.aspx</link><pubDate>Mon, 03 Apr 2006 23:58:54 GMT</pubDate><guid isPermaLink="false">91d46819-8472-40ad-a661-2c78acb4018c:567592</guid><dc:creator>Chris Jackson</dc:creator><slash:comments>2</slash:comments><comments>http://blogs.msdn.com/cjacks/comments/567592.aspx</comments><wfw:commentRss>http://blogs.msdn.com/cjacks/commentrss.aspx?PostID=567592</wfw:commentRss><wfw:comment>http://blogs.msdn.com/cjacks/rsscomments.aspx?PostID=567592</wfw:comment><description>&lt;p&gt;We have been exploring the technique of pre-rasterizing vector images, and the last time around we took a look at a particular example where pre-rendering our vector image performed better than real-time rendering when the image is small, but that the pre-rasterized version began to take much longer as the size of the controls go larger.&lt;/p&gt;
&lt;p&gt;One thing to take note of, however, is that we are pre-rasterizing to an image of the exact size and shape as our final output will be. However, we can not safely make the assumption that this will always be the case, so it's worth a short digression to investigate what might happen if we were to pre-rasterize to a different size, and then allow the framework to scale our image on our behalf.&lt;/p&gt;
&lt;p&gt;Why is this important? This is a technique that I know that people use. I have heard many people discussing the technique of creating a bitmap at the largest possible size, and then scaling it to fit the screen. (Most recently, I heard this in a discussion of rendering images for the .NET Compact Framework, in order to fit the varying screen sizes offered by any of the devices that might be using that software.) There is a performance cost involved; pre-rendering to the largest possible size and then scaling down will reduce performance. Obviously, there are more computations involved than a simple memory move. But what is this performance cost?&lt;/p&gt;
&lt;p&gt;To investigate this, I began with the same house image that I used for my last entry, and rasterized it. For one test run, I created a Bitmap object whose width and height were equal to 1/2 the width and height of the client rectangle. For another test run, I created a Bitmap object whose width and height were equal to 2 times the width and height of the client rectangle. In both cases, I rendered to the full client rectangle. Finally, as a control, I rendered to a Bitmap object of the target size. I ran 100 iterations at each of the various sizes of the image. What were the results?&lt;/p&gt;
&lt;p align="center"&gt;&lt;img src="http://blogs.msdn.com/photos/cjacks/images/567527/original.aspx" /&gt;&lt;/p&gt;
&lt;p align="center"&gt;&lt;cite&gt;Rendering Performance of Scaled Bitmaps: Size of Bitmap (as a proportion of the original) vs. Rendering Speed (in ms)&lt;/cite&gt;&lt;/p&gt;
&lt;p&gt;In both cases, the performance was worse. While the performance penalty decreased slightly over time, the change was not dramatic. The image that we scaled up from 1/2 the original size took an average of 1.46 times longer to render at each of the sizes measured. (It also looked pixelated - another shortcoming.) The image that we scaled down (the option more people would be likely to choose, since the image ends up looking much better) required an average of 3.71 times as long to render at each of the measured sizes! As the image size grows larger, this can become very significant.&lt;/p&gt;
&lt;p&gt;Of course, this is not the end of the story. These measurements assume the default interpolation mode. What happens if we change the interpolation mode on the Graphics object we are using? In every situation, scaling a bitmap from 2 times the size of the client rectangle to the size of the client rectangle reduces performance, but the result vary based on the interpolation mode. Let's take a glance at the results:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Nearest Neighbor - 314%&lt;/li&gt;
&lt;li&gt;Default - 354%&lt;/li&gt;
&lt;li&gt;Bilinear - 355%&lt;/li&gt;
&lt;li&gt;Low - 358%&lt;/li&gt;
&lt;li&gt;High Quality Bicubic - 585%&lt;/li&gt;
&lt;li&gt;High - 586%&lt;/li&gt;
&lt;li&gt;High Quality Bilinear - 1087%&lt;/li&gt;
&lt;li&gt;Bicubic - 1618%&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As you use more and more complex interpolation methods, it takes more and more time to render. (I must admit that I do not understand why an interpolation mode labeled High Quality Bicubic would outperform an interpolation mode simply labeled Bicubic by a factor of 3 to 1, but those are the results I am seeing. I ran them both several times just to make sure.)&lt;/p&gt;
&lt;p&gt;In any case, if you are hunting down ways to optimize rendering performance, eliminating resizes can potentially provide you with a drastic performance bump, especially when you are scaling down and image quality is critical.&lt;/p&gt;&lt;img src="http://blogs.msdn.com/aggbug.aspx?PostID=567592" width="1" height="1"&gt;</description><category domain="http://blogs.msdn.com/cjacks/archive/tags/User+Experience/default.aspx">User Experience</category></item><item><title>Pre-Rasterizing Vector Drawings to Improve Windows Forms Drawing Performance</title><link>http://blogs.msdn.com/cjacks/archive/2006/03/21/556721.aspx</link><pubDate>Tue, 21 Mar 2006 19:19:00 GMT</pubDate><guid isPermaLink="false">91d46819-8472-40ad-a661-2c78acb4018c:556721</guid><dc:creator>Chris Jackson</dc:creator><slash:comments>9</slash:comments><comments>http://blogs.msdn.com/cjacks/comments/556721.aspx</comments><wfw:commentRss>http://blogs.msdn.com/cjacks/commentrss.aspx?PostID=556721</wfw:commentRss><wfw:comment>http://blogs.msdn.com/cjacks/rsscomments.aspx?PostID=556721</wfw:comment><description>&lt;p&gt;In the past few posts, I have been exploring owner-drawing of vector images to create a more compelling user interface that recreates, to a high degree of fidelity, the intentions of the designers. However, as engineers, we are implementing the entirety of the user experience, and not simply the appearance. In addition to faithfully representing the visuals, our code must deliver on other promises for a compelling user experience. One of these components is performance.&lt;/p&gt;
&lt;p&gt;As I mentioned in my &lt;a href="http://blogs.msdn.com/cjacks/archive/2006/02/23/538164.aspx"&gt;first post on creating Gel Buttons&lt;/a&gt;, it is faster to move memory than it is to perform computations, which is why you can render a raster image (bitmap) faster than you can render a vector image (such as the GDI+ code that we created). In the Gel Buttons example, we were including animation, which made raster images a less tantalizing choice. However, what if we don't need animations, but we would like the scalability characteristics of vector images? We can pre-rasterize our vectors once we know the size that we are rasterizing to, and all subsequent calls to render our control can leverage the speed of rendering bitmaps (moving memory).&lt;/p&gt;
&lt;p&gt;As with many performance-related engineering decisions, here we have a trade-off between size and performance. By consuming more memory to render in advance, we can improve runtime performance. To explore this, let's use a navigation control as an example. I have created a custom control to present a navigation UI, a common idiom in applications that users are probably most familiar with from Web applications. (This idiom receives first class support in Windows Presentation Foundation, with a NavigationWindow providing this navigation UI out of the box.)&lt;/p&gt;
&lt;p align="center"&gt;&lt;img src="http://blogs.msdn.com/photos/cjacks/images/556557/original.aspx" /&gt;&lt;/p&gt;
&lt;p align="center"&gt;&lt;cite&gt;WinForms Navigation Control&lt;/cite&gt;&lt;/p&gt;
&lt;p&gt;(Once again, I am not presenting this particular design as a model of good design, but I am instead interested in the engineering techniques that we can use to represent such a button.)&lt;/p&gt;
&lt;p&gt;What I have created includes several layers of painting. There is a linear gradient background behind both buttons. Each button has a radial gradient, and there are several alpha-blended gradients on top of this button, designed both to (hopefully) make it look kind of cool and glassy.&lt;/p&gt;
&lt;p&gt;For this example, unlike our gel buttons, I have implemented 4 distinct states: normal, mouse over, mouse down, and disabled. (For the time being, I am not addressing accessibility issues.) Because I do not have animations, there are only 16 possible states for the entire control (4 states for the left button times 4 states for the right button). Let's take a look at the rendering code for this button:&lt;/p&gt;
&lt;pre&gt;
private void DrawBackground(Graphics g) {
  using (GraphicsPath p = new GraphicsPath()) {
    p.FillMode = FillMode.Winding;
    p.AddEllipse(0, 0, Width / 2, Height - 1);
    p.AddEllipse(Width / 2, 0, Width / 2 - 1, Height - 1);
    p.AddRectangle(new Rectangle(this.Width / 4, this.Height / 4,
      this.Width / 2, this.Height / 2));
    using (LinearGradientBrush b = new LinearGradientBrush(ClientRectangle,
      backgroundTop, backgroundBottom, LinearGradientMode.Vertical)) {
      g.FillPath(b, p);
    }
  }
}

private void DrawButton(Graphics g, Rectangle boundingRectangle, 
  ArrowDirection direction, ButtonState state) {

  // Draw the button
  using (GraphicsPath buttonPath = new GraphicsPath()) {
    buttonPath.AddEllipse(boundingRectangle);
    using (PathGradientBrush buttonBrush = new PathGradientBrush(buttonPath)) {
      switch (state) {
        case ButtonState.Normal:
          buttonBrush.CenterColor = buttonInnerColorNormal;
          buttonBrush.SurroundColors = new Color[] { buttonOuterColorNormal };
          break;
        case ButtonState.MouseOver:
          buttonBrush.CenterColor = buttonInnerColorMouseOver;
          buttonBrush.SurroundColors = new Color[] { buttonOuterColorMouseOver };
          break;
        case ButtonState.MouseDown:
          buttonBrush.CenterColor = buttonInnerColorMouseDown;
          buttonBrush.SurroundColors = new Color[] { buttonOuterColorMouseDown };
          break;
        case ButtonState.Disabled:
          buttonBrush.CenterColor = buttonInnerColorDisabled;
          buttonBrush.SurroundColors = new Color[] { buttonOuterColorDisabled };
          break;
      }
      g.FillPath(buttonBrush, buttonPath);
    }
  }

  // Draw the halo
  using (GraphicsPath haloPath = new GraphicsPath()) {
    haloPath.AddEllipse(boundingRectangle);
    haloRectangle.Offset(boundingRectangle.Location);
    haloPath.AddEllipse(haloRectangle);
    haloRectangle.Offset(-boundingRectangle.X, -boundingRectangle.Y);
    using (PathGradientBrush haloBrush = new PathGradientBrush(haloPath)) {
      haloBrush.CenterColor = Color.FromArgb(200, Color.White);
      haloBrush.SurroundColors = new Color[] { Color.FromArgb(0, Color.White) };
      g.FillPath(haloBrush, haloPath);
    }
  }

  if (ButtonState.MouseDown != state) {

    // Draw the moon
    using (GraphicsPath moonBottomPath = new GraphicsPath()) {
      moonRectangle.Offset(boundingRectangle.Location);
      moonBottomPath.AddEllipse(moonRectangle);
      moonRectangle.Offset(-boundingRectangle.X, -boundingRectangle.Y);
      using (Region moonRegion = new Region(moonBottomPath)) {
        using (GraphicsPath moonTopPath = new GraphicsPath()) {
          moonTopPath.AddEllipse(boundingRectangle);
          moonRegion.Complement(moonTopPath);
          using (SolidBrush moonBrush = new SolidBrush(
            Color.FromArgb(30, Color.White))) {
            g.FillRegion(moonBrush, moonRegion);
          }
        }
      }
    }

      // Draw the top reflection
    using (GraphicsPath topReflectionPath = new GraphicsPath()) {
      topReflectionRectangle.Offset(boundingRectangle.Location);
      topReflectionPath.AddEllipse(topReflectionRectangle);
      topReflectionRectangle.Offset(-boundingRectangle.X, -boundingRectangle.Y);
      using (LinearGradientBrush topReflectionBrush = new
        LinearGradientBrush(topReflectionPath.GetBounds(),
        Color.FromArgb(200, Color.White), Color.FromArgb(0, Color.White),
        LinearGradientMode.Vertical)) {
        g.FillPath(topReflectionBrush, topReflectionPath);
      }
    }

    // Draw the bottom reflection
    using (GraphicsPath bottomReflectionPath = new GraphicsPath()) {
      bottomReflectionRectangle.Offset(boundingRectangle.Location);
      bottomReflectionPath.AddEllipse(bottomReflectionRectangle);
      bottomReflectionRectangle.Offset(-boundingRectangle.X, -boundingRectangle.Y);
      using (LinearGradientBrush bottomReflectionBrush = new
        LinearGradientBrush(bottomReflectionPath.GetBounds(),
        Color.FromArgb(0, Color.White), Color.FromArgb(50, Color.White),
        LinearGradientMode.Vertical)) {
        g.FillPath(bottomReflectionBrush, bottomReflectionPath);
      }
    }

  }

  // Draw the arrow
  arrowTop.Offset(boundingRectangle.Location);
  arrowBottom.Offset(boundingRectangle.Location);
  arrowLeft.Offset(boundingRectangle.Location);
  arrowRight.Offset(boundingRectangle.Location);
  Point[] arrowHeadPoints;
  if (ArrowDirection.Left == direction) {
    arrowHeadPoints = new Point[] { arrowTop , arrowLeft, arrowBottom };
  } else {
    arrowHeadPoints = new Point[] { arrowTop, arrowRight, arrowBottom };
  }
  Point[] arrowLinePoints = new Point[] { arrowLeft, arrowRight };
  Color arrowColor = ButtonState.MouseDown == state ? Color.Gray : Color.White;
  using (SolidBrush arrowBrush = new SolidBrush(arrowColor)) {
    using (Pen arrowPen = new Pen(arrowBrush, Height / 10)) {
      arrowPen.LineJoin = LineJoin.Round;
      arrowPen.StartCap = LineCap.Round;
      arrowPen.EndCap = LineCap.Round;
      g.DrawLines(arrowPen, arrowHeadPoints);
      g.DrawLines(arrowPen, arrowLinePoints);
    }
  }
  arrowTop.Offset(-boundingRectangle.X, -boundingRectangle.Y);
  arrowBottom.Offset(-boundingRectangle.X, -boundingRectangle.Y);
  arrowLeft.Offset(-boundingRectangle.X, -boundingRectangle.Y);
  arrowRight.Offset(-boundingRectangle.X, -boundingRectangle.Y);

}
&lt;/pre&gt;
&lt;p&gt;To improve performance somewhat, I have tried to factor out a great deal of the mathematical computations used to describe the vectors we are drawing, although there is still more work that could be done to leverage this particular performance optimization technique. (I opted for simplicity of a single method to render a button, which means that I require some additional offsets.) However, the point is that I only need to perform these mathematical calculations if the control is resized - the results will be the same with every subsequent call to the OnPaint method.&lt;/p&gt;
&lt;p&gt;Following the technique I used previously, we can simply call these drawing methods when our OnPaint method is called, like so:&lt;/p&gt;
&lt;pre&gt;
protected override void OnPaint(PaintEventArgs e) {
  Graphics g = e.Graphics;
  g.SmoothingMode = SmoothingMode.HighQuality;
  g.PixelOffsetMode = PixelOffsetMode.HighQuality;
  ButtonRenderer.DrawParentBackground(g, ClientRectangle, this);
  DrawBackground(g);
  DrawButton(g, leftButtonRectangle, ArrowDirection.Left, leftButtonState);
  DrawButton(g, rightButtonRectangle, ArrowDirection.Right, rightButtonState);
}
&lt;/pre&gt;
&lt;p&gt;Now, if I instrument this code, I can manipulate the control and get a sense of how well it is performing as currently implemented. On my Tablet PC, the average time spent in OnPaint while using the real-time rasterization is 8.18ms. Since the rods in your eye have a period of integration of 100ms, and the cones have a period of integration of 10 to 15ms, this does not seem unreasonable. However, we would be wise to account for the fact that we are very unlikely to have a form where the only object we needed to paint was a navigation control - our control should provide some additional headroom to render the rest of the objects on the page, Furthermore, we may be implementing something more complex than a gel button with 11 distinct objects in order to faithfully represent the designers' intentions. Finally, keep in mind that those 8 ms represent time that your application isn't doing any meaningful business processing.&lt;/p&gt;
&lt;p&gt;Knowing that there are only 16 distinct states which we will ever need to render, we can easily rasterize these states in advance, and use the much faster approach of moving memory to render these images. Here is some code to render these states in advance (which we can call when we create the control, and also in response to a resize event):&lt;/p&gt;
&lt;pre&gt;
private void CreateImages() {
  DisposeImages();
  for (int row = 0; row &lt; 4; row++) {
    for (int col = 0; col &lt; 4; col++) {
      buttonImages[row, col] = new Bitmap(Width, Height, 
        System.Drawing.Imaging.PixelFormat.Format32bppArgb);
      using (Graphics g = Graphics.FromImage(buttonImages[row, col])) {
        g.SmoothingMode = SmoothingMode.HighQuality;
        g.PixelOffsetMode = PixelOffsetMode.HighQuality;
        ButtonRenderer.DrawParentBackground(g, ClientRectangle, this);
        DrawBackground(g);
        DrawButton(g, leftButtonRectangle, ArrowDirection.Left, (ButtonState)row);
        DrawButton(g, rightButtonRectangle, ArrowDirection.Right, (ButtonState)col);
      }
    }
  }
}
&lt;/pre&gt;
&lt;p&gt;Now that we have the images pre-rendered, the OnPaint method can be used to simply paint our pre-rendered images:&lt;/p&gt;
&lt;pre&gt;
protected override void OnPaint(PaintEventArgs e) {
  Graphics g = e.Graphics;
  g.DrawImage(buttonImages[(int)leftButtonState, (int)rightButtonState], ClientRectangle);
}
&lt;/pre&gt;
&lt;p&gt;Again, if we instrument this code, we can determine how well this method performs. On my Tablet PC, the average time spent in the OnPaint method was reduced to 0.43ms. We were able to improve the performance of our rendering code by an order of magnitude! (An 1,802% increase - not bad.)&lt;/p&gt;
&lt;p&gt;Of course, this method is not without its shortcomings. Every time that you create or resize the control, you have to wait for all 16 permutations to render before you can show anything with this naive implementation. A better approach might be to render the image for a state only when that state exists. You also have to set aside enough memory to hold all of these rendered images, which you did not have to do while rendering real-time. However, if you are implementing custom drawing and looking to make a significant increase in run-time performance, this is a technique worth considering.&lt;/p&gt;&lt;img src="http://blogs.msdn.com/aggbug.aspx?PostID=556721" width="1" height="1"&gt;</description><category domain="http://blogs.msdn.com/cjacks/archive/tags/User+Experience/default.aspx">User Experience</category></item><item><title>Passionate Developers Creating Beautiful User Interfaces</title><link>http://blogs.msdn.com/cjacks/archive/2006/03/13/550642.aspx</link><pubDate>Mon, 13 Mar 2006 21:48:00 GMT</pubDate><guid isPermaLink="false">91d46819-8472-40ad-a661-2c78acb4018c:550642</guid><dc:creator>Chris Jackson</dc:creator><slash:comments>0</slash:comments><comments>http://blogs.msdn.com/cjacks/comments/550642.aspx</comments><wfw:commentRss>http://blogs.msdn.com/cjacks/commentrss.aspx?PostID=550642</wfw:commentRss><wfw:comment>http://blogs.msdn.com/cjacks/rsscomments.aspx?PostID=550642</wfw:comment><description>&lt;p&gt;In a comment to my &lt;a href="http://blogs.msdn.com/cjacks/archive/2006/03/08/546259.aspx"&gt;final post on gel buttons&lt;/a&gt;, the orginality and creativity of my design was, shall we say, called into question. Obviously, I have miscommunicated my intentions here, as evidenced by the tone and directness of the feedback.&lt;/p&gt;
&lt;p&gt;I am not a designer. I am not trained to be one, nor do I have any focussed inate talent in that domain. Therefore, my purpose was not to propose the notion of a gel button as being an exciting new design innovation. This is not a new design recommendation for implementing buttons from either me or my employer.&lt;/p&gt;
&lt;p&gt;What is the point, then, of demonstrating how to create buttons that look like this, if I am not recommending that you create buttons that look like this?&lt;/p&gt;
&lt;p&gt;I am not attempting to recommend a solution to a UI design problem, but instead to convey some approaches to implementing a solution to a UI design problem that somebody has provided. I believe that being able to represent, with great fidelity, the designs created by those who actually do have design talent will become an important part of the software engineering discipline. Often, implementation of an application is determined by what can be created quickly in the development tool's form designer, with the majority of the engineering effort devoted to implementing the functionality behind this UI. In many cases, this is completely appropriate. However, as application complexity continues to increase, it becomes more and more important to faithfully represent the intentions of the designers who mock up the application, as these designs will explicity target making the application either easier or more enjoyable to use. Close enough is becoming more and more insufficient as complexity, as well as the quality and quantity of competition, increases.&lt;/p&gt;
&lt;p&gt;This is not limited to "bling" applications, such as Windows Media Player, or any number of other consumer-focussed applications. My tax software, for example, renders its own buttons. (It uses the increasingly popular "two-tone" buttons.) It increases the feel of quality. Even for non-commercial line-of-business appications, there are many problems where the approximations provided by the built-in controls are not quite accurate enough, and could be enhanced with both design and engineering talent. Usability should not be an afterthought.&lt;/p&gt;
&lt;p&gt;Applications hopefully provide a solution to a particular problem domain. The challenge to the designer is to understand the core concepts that define the solution space, and present UI that provide a solution for this problem domain in the most intuitive way possible. The challenge to the engineer is to implement the designed solution with the maximum fidelity possible.&lt;/p&gt;
&lt;p align="center"&gt;&lt;a href="http://www.microsoft.com/library/media/1033/office/images/preview/ui/UI_1_800x600.jpg" target="_blank"&gt;&lt;img src="http://www.microsoft.com/library/media/1033/office/images/preview/ui/UI_1_400x300.jpg" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p align="center"&gt;&lt;a href="http://www.microsoft.com/office/preview/uioverview.mspx"&gt;&lt;cite&gt;The new Microsoft Office system user interface&lt;/cite&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Brad Weed, the Product Design Manager of the Office Design Group, &lt;a href="http://blogs.msdn.com/jensenh/archive/2006/03/10/548542.aspx"&gt;describes the relationship between the design displine and the engineering discipline in the Office team&lt;/a&gt; (he was a guest writer on Jensen Harris' fabulous &lt;a href="http://blogs.msdn.com/jensenh/default.aspx"&gt;An Office User Interface Blog&lt;/a&gt;):&lt;/p&gt;
&lt;cite&gt;We have an amazingly talented set of designers that spend inordinate amounts of time camped out with passionate developers devising clever ways to pull off these beautiful visual designs. And every decision intended to make the product more desirable impacts the usability and utility of the product in some crazy unpredictable way. This only complicates the process, yielding even bigger collaborations.&lt;/cite&gt;
&lt;p&gt;So, essentially, my point is that making great designs come to life is important to maximize the usability and experience of your applications. This is a valuable engineering discipline. My example of gel buttons was somewhat arbitrary (coming directly from a link I read in another blog) and implemented just to start giving a taste of how to approach these problems from an engineering perspective.&lt;/p&gt;
&lt;p&gt;Of course, that's not the only reason I delve into the implementation of user interface and experience.&lt;/p&gt;
&lt;p&gt;I love pixels.&lt;/p&gt;
&lt;p&gt;In the end, it's all about raw pixels. If there is one component of my computer that I am going to over-spend on, it is the monitor. Why? That's what I have to look at all day. It feels better to me to use something that has a sense of being well designed, and I want to deliver that sense of satisfaction to people who use the software that I create or help my customers create. (Incidentally, while the initial Beta 1 release of Office 2007 was "neat", the design of the Beta 1 refresh makes it finally feel satisfying. I love good design.)&lt;/p&gt;
&lt;p&gt;I am hoping to share my joy of pixels, and to hopefully incite some respect for investment in faithfully reproducing design. In the end, the engineering goal is to bring static images and interactive prototypes to life, honoring both the intent of the design and the limitations of the real world. Design is finally coming to prominence at Microsoft. Windows Vista and Office 2007 are two products that, to me, clearly demonstrate the value of excellent design, and they are just the beginning. Coming soon, we also have the Windows Presentation Foundation, comlete with tools for designers to implement this UI code. However, even with the power provided by the Expression Interactive Designer ("Sparkle"), engineering talent for implementing fantastic UI was still required. (Check out the dynamic SmoothMove control custom implemented for the &lt;a href="http://blogs.msdn.com/expression/archive/2006/01/24/517010.aspx"&gt;Flickr Browser&lt;/a&gt;.) These new products will emphasize the workflow between designer and developer, so we will no longer be limited to passing Photoshop images and Flash animations back and forth - we can finally collaborate.&lt;/p&gt;
&lt;p&gt;Working in concert with great designers, we can create some fantastic user experiences on top of powerful products. I'll leave the design work to those more talented than I am, and stay behind the scenes writing the code to push pixels around.&lt;/p&gt;&lt;img src="http://blogs.msdn.com/aggbug.aspx?PostID=550642" width="1" height="1"&gt;</description><category domain="http://blogs.msdn.com/cjacks/archive/tags/User+Experience/default.aspx">User Experience</category></item><item><title>Creating Gel Buttons with Windows Forms : Part 3</title><link>http://blogs.msdn.com/cjacks/archive/2006/03/08/546259.aspx</link><pubDate>Wed, 08 Mar 2006 19:45:05 GMT</pubDate><guid isPermaLink="false">91d46819-8472-40ad-a661-2c78acb4018c:546259</guid><dc:creator>Chris Jackson</dc:creator><slash:comments>6</slash:comments><comments>http://blogs.msdn.com/cjacks/comments/546259.aspx</comments><wfw:commentRss>http://blogs.msdn.com/cjacks/commentrss.aspx?PostID=546259</wfw:commentRss><wfw:comment>http://blogs.msdn.com/cjacks/rsscomments.aspx?PostID=546259</wfw:comment><description>&lt;p&gt;Based on a few comments (both directly on the blog, as well as internal email), I have decided to post a third entry to further develop our Gel Buttons. (Not that I am trying to belabor the topic, but I think there are some important things to learn from our simple little buttons.)&lt;/p&gt;
&lt;p&gt;First of all, the implementation in parts 1 and 2 did include some pixel offsets. Let's look at the effect that this decision has. First, the radius of the ellipses (circles) forming the corners of the rounded rectanges were always exactly 5 pixels. The highight was always positioned 2 pixels from the edge of the button. The outline was always 1 pixel wide. So, while the button itself, and the highlight, would always fill the entirety of the bounding rectangle of the control, as the control became larger (or the DPI became higher), the corners would look less rounded, the highlight would get closer and closer to the edge of the control, and the single pixel outline would look less and less significant.&lt;/p&gt;
&lt;p&gt;(Note that, as with most discussions of pixels with Windows Forms, we are discussing logical pixels. If, for example, you were to modify the graphics object using &lt;code&gt;g.ScaleTransform(2.0F, 2.0F);&lt;/code&gt; then the outline would be 2 pixels wide.)&lt;/p&gt;
&lt;p&gt;We can truly leverage our vector graphics platform here by setting these values as a percentage of the control size, as we are already doing with the width and height of both the background rectangle and the highlight rectangle. So, I have modified our code to do exactly that, enabling a very smooth scaling to any size.&lt;/p&gt;
&lt;p&gt;What are some of the things to watch for when doing this? First of all, you have to determine what exactly you are going to use as the basis for your scaling. I have chosen to select the smaller of either width or height. Basing our scaling on one or the other can cause unfortunate consequences when a consumer decides to create either an unusually wide or unusually tall button. Basing it on the smaller value allows the button to behave as we expect when the button is scaled proportionately, while also affording expected behavior when scaling non-proportionately.&lt;/p&gt;
&lt;p&gt;Another interesting consequence comes with the outline. At 1 pixel wide, we are certain that this pixel will fit inside of our container. Once that number begins to grow, then the center of this thick line becomes the bounds of our rectangle, which trims the edges disproportionately from the rounded corners (which have more pixels to spare until they reach the bounds of the container). We can fix that by setting the alignment of the pen to &lt;code&gt;PenAlignment.Inset&lt;/code&gt;, which uses our outer boundary as the outer boundary of our drawing, rather than the center.&lt;/p&gt;
&lt;p&gt;Now that we have addressed scaling more thoroughly, let's address another important issue: accessibility. Since we are deriving our control from Button, we inherit a number of accessibility features for free. Our control exposes AccessibleDescription, AccessibleName, AccessibleRole, TabIndex, Text, and Font Size properties. Consequently, our button will be compatible with screen readers and other features that are designed to help those who depend on these types of software.&lt;/p&gt;
&lt;p&gt;What about those who are not using a mouse, either out of necessity or by choice? We were not providing a particularly good experience to them - we only drew our selection highlights and click effects in response to mouse events. So, let's add some keyboard handlers to have our buttons highlight when they have the focus, return to normal when they lose focus, and appear pressed when the user presses the space bar. This improves the experience with the buttons even more.&lt;/p&gt;
&lt;p&gt;Finally, let's consider colors. Since we are allowing the developer to specify the color of the buttons (or accept the default colors), rather than taking these colors from the operating system, we are not responding to the user's preferences regarding which colors help them see better. If they have selected a high contrast color scheme, our gel buttons still appear using the colors the developer specified. What can we do about this?&lt;/p&gt;
&lt;p&gt;One option is to start with system colors, which will detect the user's preferences. However, system colors don't provide us with two coordinated gradient values. We can overcome this by starting with a single system color, looking at that system color, and then modifying the brightness to obtain a light and dark version of that shade. Unforunately, that is not as straightforward as it seems. Although you can get hue, saturation, and brightness values from an instance of the Color structure, there is no FromHsb method that would allow us to construct a new Color structure using a variation of the brightness value. This work must be done by us. If we choose to go that route, then we also need to decide which system color should be the basis for our gradient. ButtonFace seems the logical choice, but generally buttons are not as colorful as what we are trying to create here. Perhaps SystemColors.ActiveCaption?&lt;/p&gt;
&lt;p&gt;The other option is to specifically check to see if the user has specified a high contrast color scheme. Fortunately, the System.Windows.Forms.SystemInformation class provides a HighContrast property that allows us to check to see if the user has selected this option, and if so to respond accordingly. We can check this variable and custom render our own choices for high contrast colors. This is the option I have implemented here.&lt;/p&gt;
&lt;p&gt;After addressing these issues, there are a couple of additional features that I believe are worth implementing. First is the notion of the default button. If you look at many dialog boxes, you will see some indication of which button is the default button - the button that will respond immediately if you press the enter key. We would like to offer a similar visual cue in our button class, which we have done here using a subtle white highlight around the edge of the button (using a PathGradientBrush).&lt;/p&gt;
&lt;p&gt;Another important feature is visually indicating when a button is disabled. As it was, a disabled button would respond to mouse and keyboard events in exactly the same way that an enabled button would. Obviously, announcing "hey, you can click me!" when, in fact, you can't, provides a negative user experience.&lt;/p&gt;
&lt;p&gt;With default buttons, once again we would like to respect the decision of the developer regarding colors, and simply modify these colors to present them as a grayscale of that color. Fortunately, grayscale is an edge case that doesn't require any sophisticated transformations. We can simply look at the brightness of the color we are using, and construct a new Color structure using that brightness for red, green and blue. (Note that GetBrightness returns a value from 0 to 1, so we need to multiply by 255 first.)&lt;/p&gt;
&lt;p&gt;Let's take a look at the results:&lt;/p&gt;
&lt;p align="center"&gt;&lt;img src="http://blogs.msdn.com/photos/cjacks/images/546254/original.aspx" /&gt;&lt;/p&gt;
&lt;p align="center"&gt;&lt;cite&gt;Windows Forms Gel Buttons - Third Revision&lt;/cite&gt;&lt;/p&gt;
&lt;p&gt;Finally, let's take a look at the code:&lt;/p&gt;
&lt;pre&gt;
namespace GelButtons {

  using System;
  using System.ComponentModel;
  using System.Drawing;
  using System.Drawing.Drawing2D;
  using System.Windows.Forms;

  class GelButton : Button {

    #region Fields

    private Color gradientTop = Color.FromArgb(255, 44, 85, 177);
    private Color gradientBottom = Color.FromArgb(255, 153, 198, 241);
    private Color paintGradientTop;
    private Color paintGradientBottom;
    private Color paintForeColor;
    private Rectangle buttonRect;
    private Rectangle highlightRect;
    private int rectCornerRadius;
    private float rectOutlineWidth;
    private int highlightRectOffset;
    private int defaultHighlightOffset;
    private int highlightAlphaTop = 255;
    private int highlightAlphaBottom;
    private Timer animateButtonHighlightedTimer = new Timer();
    private Timer animateResumeNormalTimer = new Timer();
    private bool increasingAlpha;

    #endregion

    #region Properties

    [Category("Appearance")]
    [Description("The color to use for the top portion of the gradient fill of the component.")]
    [DefaultValue(typeof(Color), "0x2C55B1")]
    public Color GradientTop {
      get {
        return gradientTop;
      }
      set {
        gradientTop = value;
        SetPaintColors();
        Invalidate();
      }
    }

    [Category("Appearance")]
    [Description("The color to use for the bottom portion of the gradient fill of the component.")]
    [DefaultValue(typeof(Color), "0x99C6F1")]
    public Color GradientBottom {
      get {
        return gradientBottom;
      }
      set {
        gradientBottom = value;
        SetPaintColors();
        Invalidate();
      }
    }

    public override Color ForeColor {
      get {
        return base.ForeColor;
      }
      set {
        base.ForeColor = value;
        SetPaintColors();
        Invalidate();
      }
    }

    #endregion

    #region Initialization and Modification

    protected override void OnCreateControl() {
      SuspendLayout();
      SetControlSizes();
      SetPaintColors();
      InitializeTimers();
      base.OnCreateControl();
      ResumeLayout();
    }

    protected override void OnResize(EventArgs e) {
      SetControlSizes();
      this.Invalidate();
      base.OnResize(e);
    }

    private void SetControlSizes() {
      int scalingDividend = Math.Min(ClientRectangle.Width, ClientRectangle.Height);
      buttonRect = new Rectangle(ClientRectangle.X, ClientRectangle.Y, 
ClientRectangle.Width - 1, ClientRectangle.Height - 1);
      rectCornerRadius = Math.Max(1, scalingDividend / 10);
      rectOutlineWidth = Math.Max(1, scalingDividend / 50);
      highlightRect = new Rectangle(ClientRectangle.X, ClientRectangle.Y, 
ClientRectangle.Width - 1, (ClientRectangle.Height - 1) / 2);
      highlightRectOffset = Math.Max(1, scalingDividend / 35);
      defaultHighlightOffset = Math.Max(1, scalingDividend / 35);
    }

    protected override void OnEnabledChanged(EventArgs e) {
      if (!Enabled) {
        animateButtonHighlightedTimer.Stop();
        animateResumeNormalTimer.Stop();
      }
      SetPaintColors();
      Invalidate();
      base.OnEnabledChanged(e);
    }

    private void SetPaintColors() {
      if (Enabled) {
        if (SystemInformation.HighContrast) {
          paintGradientTop = Color.Black;
          paintGradientBottom = Color.Black;
          paintForeColor = Color.White;
        } else {
          paintGradientTop = gradientTop;
          paintGradientBottom = gradientBottom;
          paintForeColor = ForeColor;
        }
      } else {
        if (SystemInformation.HighContrast) {
          paintGradientTop = Color.Gray;
          paintGradientBottom = Color.White;
          paintForeColor = Color.Black;
        } else {
          int grayscaleColorTop = (int)(gradientTop.GetBrightness() * 255);
          paintGradientTop = Color.FromArgb(grayscaleColorTop, 
grayscaleColorTop, grayscaleColorTop);
          int grayscaleGradientBottom = (int)(gradientBottom.GetBrightness() * 255);
          paintGradientBottom = Color.FromArgb(grayscaleGradientBottom, 
grayscaleGradientBottom, grayscaleGradientBottom);
          int grayscaleForeColor = (int)(ForeColor.GetBrightness() * 255);
          if (grayscaleForeColor &gt; 255 / 2) {
            grayscaleForeColor -= 60;
          } else {
            grayscaleForeColor += 60;
          }
          paintForeColor = Color.FromArgb(grayscaleForeColor, grayscaleForeColor, grayscaleForeColor);
        }
      }
    }

    private void InitializeTimers() {
      animateButtonHighlightedTimer.Interval = 20;
      animateButtonHighlightedTimer.Tick += new EventHandler(animateButtonHighlightedTimer_Tick);
      animateResumeNormalTimer.Interval = 5;
      animateResumeNormalTimer.Tick += new EventHandler(animateResumeNormalTimer_Tick);
    }

    #endregion

    #region Custom Painting

    protected override void OnPaint(PaintEventArgs pevent) {
      Graphics g = pevent.Graphics;
      ButtonRenderer.DrawParentBackground(g, ClientRectangle, this);
      // Paint the outer rounded rectangle
      g.SmoothingMode = SmoothingMode.AntiAlias;
      using (GraphicsPath outerPath = RoundedRectangle(buttonRect, rectCornerRadius, 0)) {
        using (LinearGradientBrush outerBrush = new LinearGradientBrush(buttonRect, 
paintGradientTop, paintGradientBottom, LinearGradientMode.Vertical)) {
          g.FillPath(outerBrush, outerPath);
        }
        using (Pen outlinePen = new Pen(paintGradientTop, rectOutlineWidth)) {
          outlinePen.Alignment = PenAlignment.Inset;
          g.DrawPath(outlinePen, outerPath);
        }
      }
      // If this is the default button, paint an additional highlight
      if (IsDefault) {
        using (GraphicsPath defaultPath = new GraphicsPath()) {
          defaultPath.AddPath(RoundedRectangle(buttonRect, rectCornerRadius, 0), false);
          defaultPath.AddPath(RoundedRectangle(buttonRect, rectCornerRadius, defaultHighlightOffset), false);
          using (PathGradientBrush defaultBrush = new PathGradientBrush(defaultPath)) {
            defaultBrush.CenterColor = Color.FromArgb(50, Color.White);
            defaultBrush.SurroundColors = new Color[] { Color.FromArgb(100, Color.White) };
            g.FillPath(defaultBrush, defaultPath);
          }
        }
      }
      // Paint the gel highlight
      using (GraphicsPath innerPath = RoundedRectangle(highlightRect, rectCornerRadius, highlightRectOffset)) {
        using (LinearGradientBrush innerBrush = new LinearGradientBrush(highlightRect, 
Color.FromArgb(highlightAlphaTop, Color.White),
Color.FromArgb(highlightAlphaBottom, Color.White), LinearGradientMode.Vertical)) {
          g.FillPath(innerBrush, innerPath);
        }
      }
      // Paint the text
      TextRenderer.DrawText(g, Text, Font, buttonRect, paintForeColor, Color.Transparent, 
TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis);
    }

    private static GraphicsPath RoundedRectangle(Rectangle boundingRect, int cornerRadius, int margin) {
      GraphicsPath roundedRect = new GraphicsPath();
      roundedRect.AddArc(boundingRect.X + margin, boundingRect.Y + margin, cornerRadius * 2, 
cornerRadius * 2, 180, 90);
      roundedRect.AddArc(boundingRect.X + boundingRect.Width - margin - cornerRadius * 2, 
boundingRect.Y + margin, cornerRadius * 2, cornerRadius * 2, 270, 90);
      roundedRect.AddArc(boundingRect.X + boundingRect.Width - margin - cornerRadius * 2, 
boundingRect.Y + boundingRect.Height - margin - cornerRadius * 2, cornerRadius * 2, cornerRadius * 2, 0, 90);
      roundedRect.AddArc(boundingRect.X + margin, 
boundingRect.Y + boundingRect.Height - margin - cornerRadius * 2, 
cornerRadius * 2, cornerRadius * 2, 90, 90);
      roundedRect.AddLine(boundingRect.X + margin, 
boundingRect.Y + boundingRect.Height - margin - cornerRadius * 2, 
boundingRect.X + margin, boundingRect.Y + margin + cornerRadius);
      roundedRect.CloseFigure();
      return roundedRect;
    }

    #endregion

    #region Mouse and Keyboard Interaction

    protected override void OnMouseEnter(EventArgs e) {
      HighlightButton();
      base.OnMouseEnter(e);
    }

    protected override void OnGotFocus(EventArgs e) {
      HighlightButton();
      base.OnGotFocus(e);
    }

    private void HighlightButton() {
      if (Enabled) {
        animateResumeNormalTimer.Stop();
        animateButtonHighlightedTimer.Start();
      }
    }

    private void animateButtonHighlightedTimer_Tick(object sender, EventArgs e) {
      if (increasingAlpha) {
        if (100 &lt;= highlightAlphaBottom) {
          increasingAlpha = false;
        } else {
          highlightAlphaBottom += 5;
        }
      } else {
        if (0 &gt;= highlightAlphaBottom) {
          increasingAlpha = true;
        } else {
          highlightAlphaBottom -= 5;
        }
      }
      Invalidate();
    }

    protected override void OnMouseLeave(EventArgs e) {
      ResumeNormalButton();
      base.OnMouseLeave(e);
    }

    protected override void OnLostFocus(EventArgs e) {
      ResumeNormalButton();
      base.OnLostFocus(e);
    }

    private void ResumeNormalButton() {
      if (Enabled) {
        animateButtonHighlightedTimer.Stop();
        animateResumeNormalTimer.Start();
      }
    }

    private void animateResumeNormalTimer_Tick(object sender, EventArgs e) {
      bool modified = false;
      if (highlightAlphaBottom &gt; 0) {
        highlightAlphaBottom -= 5;
        modified = true;
      }
      if (highlightAlphaTop &lt; 255) {
        highlightAlphaTop += 5;
        modified = true;
      }
      if (!modified) {
        animateResumeNormalTimer.Stop();
      }
      Invalidate();
    }

    protected override void OnMouseDown(MouseEventArgs mevent) {
      PressButton();
      base.OnMouseDown(mevent);
    }

    protected override void OnKeyDown(KeyEventArgs kevent) {
      if (kevent.KeyCode == Keys.Space || kevent.KeyCode == Keys.Return) {
        PressButton();
      }
      base.OnKeyDown(kevent);
    }

    private void PressButton() {
      if (Enabled) {
        animateButtonHighlightedTimer.Stop();
        animateResumeNormalTimer.Stop();
        highlightRect.Location = new Point(0, ClientRectangle.Height / 2);
        highlightAlphaTop = 0;
        highlightAlphaBottom = 200;
        Invalidate();
      }
    }

    protected override void OnMouseUp(MouseEventArgs mevent) {
      ReleaseButton();
      if (DisplayRectangle.Contains(mevent.Location)) {
        HighlightButton();
      }
      base.OnMouseUp(mevent);
    }

    protected override void OnKeyUp(KeyEventArgs kevent) {
      if (kevent.KeyCode == Keys.Space || kevent.KeyCode == Keys.Return) {
        ReleaseButton();
        if (IsDefault) {
          HighlightButton();
        }
      }
      base.OnKeyUp(kevent);
    }

    protected override void OnMouseMove(MouseEventArgs mevent) {
      if (Enabled &amp;&amp; (mevent.Button &amp; MouseButtons.Left) == MouseButtons.Left &amp;&amp; 
!ClientRectangle.Contains(mevent.Location)) {
        ReleaseButton();
      }
      base.OnMouseMove(mevent);
    }

    private void ReleaseButton() {
      if (Enabled) {
        highlightRect.Location = new Point(0, 0);
        highlightAlphaTop = 255;
        highlightAlphaBottom = 0;
      }
    }

    #endregion

  }
}
&lt;/pre&gt;
&lt;p&gt;Next, let's move on to something other than our illustrious buttons!&lt;/p&gt;&lt;img src="http://blogs.msdn.com/aggbug.aspx?PostID=546259" width="1" height="1"&gt;</description><category domain="http://blogs.msdn.com/cjacks/archive/tags/User+Experience/default.aspx">User Experience</category></item><item><title>Creating Gel Buttons with Windows Forms : Part 2</title><link>http://blogs.msdn.com/cjacks/archive/2006/03/01/541384.aspx</link><pubDate>Wed, 01 Mar 2006 20:40:30 GMT</pubDate><guid isPermaLink="false">91d46819-8472-40ad-a661-2c78acb4018c:541384</guid><dc:creator>Chris Jackson</dc:creator><slash:comments>9</slash:comments><comments>http://blogs.msdn.com/cjacks/comments/541384.aspx</comments><wfw:commentRss>http://blogs.msdn.com/cjacks/commentrss.aspx?PostID=541384</wfw:commentRss><wfw:comment>http://blogs.msdn.com/cjacks/rsscomments.aspx?PostID=541384</wfw:comment><description>&lt;p&gt;In our last episode, we went through the process of creating the static rendering of a gel button using Windows Forms. (The code I provided was developed using the .NET Framework 2.0, although at least one person has pointed out that it doesn't take much work to port this code to the 1.x version of the framework.)  At the end of this, I mentioned that we could have achieved the same effect without having to write any code at all using a static image. I want to clarify this statement, as there could potentially be a difference.&lt;/p&gt;
&lt;p&gt;What we have created is a vector drawing, that will smoothly scale to any size that you want to render a button. If you choose to implement this as a static image, not all image representations are vector representations. In fact, most of the most familiar image types are raster image types, such as bmp, jpg, gif, or png. If you needed to resize the image, you would be forced to resort to raster techniques, such as bicubic, bilinear, or nearest neighbor interpolation, in order to stretch the image to the target size - this pretty much always makes it look worse than the original image. In addition, your application would need to allocate space to house this raster image as a resource, increasing the size of your binary.&lt;/p&gt;
&lt;p&gt;Not all image types are raster images, however. Windows Metafiles (wmf) and Enhanced Windows Metafiles (emf) are both vector image types. We could potentially have implemented this static image as an emf. In fact, if we had, our application would have virtually the same characteristics. You can think of emf files as being, at some level, a markup language for GDI+ drawing such as what we are doing here. If you enumerate through a metafile, you will see a series of records such as EMREXTCREATEPEN, EMRGRADIENTFILL, and EMRFILLRGN - each of these records maps directly to GDI+ commands, in exactly the same way that our System.Drawing code maps to GDI+ commands. So, from a general standpoint, it is possible to represent a vector image as a metafile rather than in code as we have done here and achieve fundamentally equivalent results in terms of both file size and drawing performance.&lt;/p&gt;
&lt;p&gt;Of course, when I refer to metafiles as being like a markup language, there are some key differences between it and a true markup language, such as how you may use the XAML markup language to render an image in the Windows Presentation Foundation. First, metafiles are procedural - they execute from top to bottom, and do not include a notion of nesting other than by the order of commands executed. Second, they are not presented in a human readable form. They are specifically encoded to a binary format.&lt;/p&gt;
&lt;p&gt;Some of you may be considering putting together a metafile generator to capture some of the great drawings that you may have implemented in code. A single file certainly is much easier to distribute than a block of code - and this is convenient packaging. This is not as straightforward as you might hope, as the .NET Framework documentation tells us.&lt;/p&gt;
&lt;cite&gt;When you use the Save method to save a graphic image as a Windows Metafile Format (WMF) or Enhanced Metafile Format (EMF) file, the resulting file is saved as a Portable Network Graphics (PNG) file instead. This behavior occurs because the component of the does not have an encoder that you can use to save files as .wmf or .emf files.&lt;/cite&gt;
&lt;p&gt;Of course, that does not mean that we can't do it, but the fact that it is not obvious and ubiquitous probably explains why more software doesn't provide metafile output today. (For example, you can create a vector drawing in using the January 2006 CTP of Microsoft Expression Graphic Designer, but you can't save your drawing as a metafile.) We may come back to this in a later entry.&lt;/p&gt;
&lt;p&gt;Of course, now that we have talked around our static vector image for a while, I think it's about time that we brought our button to life. Static images of buttons are only good for screen shots, and are not that interesting in the real world! As we said before, we would like our button to be suitably pliant, as well as taking advantage of the smart client platform. This is the result we are aiming for:&lt;/p&gt;
&lt;p align="center"&gt;&lt;img src="http://blogs.msdn.com/photos/cjacks/images/541329/original.aspx" /&gt;&lt;/p&gt;
&lt;p align="center"&gt;&lt;cite&gt;Windows Forms Gel Buttons : Second Revision&lt;/cite&gt;&lt;/p&gt;
&lt;p&gt;First, let's consider how we want to convey our pliancy, and how we could implement that in code. What I have chosen to do here is to manipulate the highlight, rather than manipulating any of the colors on the button. This makes it easy to support any background color that my consumer may use. Regardless of whether the button is the default blue I am using, or if it has been manipulated to be red, green, or any other color, I am always using a white highlight.&lt;/p&gt;
&lt;p&gt;What do we want to do with that highlight? When you mouse over the button, a common approach is to make it lighter. This is fairly straightforward, as we merely need to increase the alpha channel for the bottom portion of the highlight from 0 to a higher value, allowing more of the white to shine through. However, since we have complete control of the graphics platform (a major advantage of the smart client), why limit ourselves to merely rendering a second, slightly brighter, static image? We can instead use animation to make our button pulsate, conveying the sense of pliancy through more than a single image change.&lt;/p&gt;
&lt;p&gt;Animation will become easier with the Windows Presentation Foundation, but we can achieve the same effect today. What is animation but a series of static images that change smoothly over time? This is how movies and TV work. But how do we add a third time dimension to our code? Timers are one way to do that, and this is what I have chosen to do here.&lt;/p&gt;
&lt;p&gt;Note that there are several options to choose from for a Timer object, with instances to choose from in the System.Threading, System.Timers, and System.Windows.Forms namespaces. I won't go into the details of when to choose which timer here (there is an &lt;a href="http://msdn.microsoft.com/msdnmag/issues/04/02/TimersinNET/"&gt;introduction in MSDN Magazine&lt;/a&gt; for those who are interested), but I have chosen the timer object in the System.Windows.Forms namespace. If the primary UI thread is busy doing something else (handling the remainder of the windows messages coming in to the application), then losing a beat in my animation is not going to be my primary concern. Since I am doing UI work, I would need to invoke onto the primary UI thread anyway. Of course, if you unnecessarily block the primary UI thread with synchronous IO or network calls, then I will have yet another visual representation of this design deficiency.&lt;/p&gt;
&lt;p&gt;Using this timer, I can gradually pulsate my button - adjusting the alpha of the bottom of the highlight. You can modify the properties of the timer to either speed this up or slow it down according to your tastes.&lt;/p&gt;
&lt;p&gt;I used a second timer to return to normal, so that moving away from a button smoothly animates back to normal, rather than instantly and jarringly returning to its normal rendering state. Once I have reached that state, then the timer tick event can turn the timer off itself. Fairly straightforward.&lt;/p&gt;
&lt;p&gt;Finally, we handle the pressed state. If I have a light coming from above in a button sticking up, then it serves to reason that this light would strike the bottom portion of that button when it is depressed, although that light would be somewhat less strong. So, here I merely moved the highlight to the bottom of the screen, reversed the order of the alpha manipulation, and modified the final alpha values to be less bright overall.&lt;/p&gt;
&lt;p&gt;That being said, let's take a look at the code:&lt;/p&gt;
&lt;pre&gt;
namespace GelButtons {

  using System;
  using System.ComponentModel;
  using System.Drawing;
  using System.Drawing.Drawing2D;
  using System.Windows.Forms;

  class GelButton : Button {

    private Color gradientTop = Color.FromArgb(255, 44, 85, 177);
    private Color gradientBottom = Color.FromArgb(255, 153, 198, 241);
    private Rectangle buttonRect;
    private int highlightAlphaTop = 255;
    private int highlightAlphaBottom;
    private Rectangle highlightRect;
    private Timer animateMouseOverTimer = new Timer();
    private Timer animateResumeNormalTimer = new Timer();
    private bool increasingAlpha;

    [Category("Appearance"), 
Description("The color to use for the top portion of the gradient fill of the component.")]
    public Color GradientTop {
      get {
        return gradientTop;
      }
      set {
        gradientTop = value;
        Invalidate();
      }
    }

    [Category("Appearance"),
Description("The color to use for the bottom portion of the gradient fill of the component.")]
    public Color GradientBottom {
      get {
        return gradientBottom;
      }
      set {
        gradientBottom = value;
        Invalidate();
      }
    }

    protected override void OnCreateControl() {
      SuspendLayout();
      base.OnCreateControl();
      buttonRect = new Rectangle(ClientRectangle.X, ClientRectangle.Y, 
ClientRectangle.Width - 1, ClientRectangle.Height - 1);
      highlightRect = new Rectangle(ClientRectangle.X, ClientRectangle.Y, 
ClientRectangle.Width - 1, ClientRectangle.Height / 2 - 1);
      animateMouseOverTimer.Interval = 20;
      animateMouseOverTimer.Tick += new EventHandler(animateMouseOverTimer_Tick);
      animateResumeNormalTimer.Interval = 5;
      animateResumeNormalTimer.Tick += new EventHandler(animateResumeNormalTimer_Tick);
      ResumeLayout();
    }

    protected override void OnPaint(PaintEventArgs pevent) {
      Graphics g = pevent.Graphics;
      // Fill the background
      ButtonRenderer.DrawParentBackground(g, ClientRectangle, this);
      // Paint the outer rounded rectangle
      g.SmoothingMode = SmoothingMode.AntiAlias;
      using (GraphicsPath outerPath = RoundedRectangle(buttonRect, 5, 0)) {
        using (LinearGradientBrush outerBrush = 
new LinearGradientBrush(buttonRect, gradientTop, gradientBottom, LinearGradientMode.Vertical)) {
          g.FillPath(outerBrush, outerPath);
        }
        using (Pen outlinePen = new Pen(gradientTop)) {
          g.DrawPath(outlinePen, outerPath);
        }
      }
      // Paint the highlight rounded rectangle
      using (GraphicsPath innerPath = RoundedRectangle(highlightRect, 5, 2)) {
        using (LinearGradientBrush innerBrush = new LinearGradientBrush(highlightRect, 
Color.FromArgb(highlightAlphaTop, Color.White), Color.FromArgb(highlightAlphaBottom, Color.White), 
LinearGradientMode.Vertical)) {
          g.FillPath(innerBrush, innerPath);
        }
      }
      // Paint the text
      TextRenderer.DrawText(g, Text, Font, buttonRect, ForeColor, Color.Transparent, 
TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis);
    }

    private static GraphicsPath RoundedRectangle(Rectangle boundingRect, int cornerRadius, int margin) {
      GraphicsPath roundedRect = new GraphicsPath();
      roundedRect.AddArc(boundingRect.X + margin, boundingRect.Y + margin, 
cornerRadius * 2, cornerRadius * 2, 180, 90);
      roundedRect.AddArc(boundingRect.X + boundingRect.Width - margin - cornerRadius * 2, 
boundingRect.Y + margin, cornerRadius * 2, cornerRadius * 2, 270, 90);
      roundedRect.AddArc(boundingRect.X + boundingRect.Width - margin - cornerRadius * 2, 
boundingRect.Y + boundingRect.Height - margin - cornerRadius * 2, cornerRadius * 2, cornerRadius * 2, 0, 90);
      roundedRect.AddArc(boundingRect.X + margin, boundingRect.Y + boundingRect.Height - 
margin - cornerRadius * 2, cornerRadius * 2, cornerRadius * 2, 90, 90);
      roundedRect.CloseFigure();
      return roundedRect;
    }

    protected override void OnMouseEnter(EventArgs e) {
      animateResumeNormalTimer.Stop();
      animateMouseOverTimer.Start();
      base.OnMouseEnter(e);
    }

    protected override void OnMouseLeave(EventArgs e) {
      animateMouseOverTimer.Stop();
      animateResumeNormalTimer.Start();
      base.OnMouseLeave(e);
    }

    protected override void OnMouseDown(MouseEventArgs mevent) {
      animateMouseOverTimer.Stop();
      animateResumeNormalTimer.Stop();
      highlightRect.Location = new Point(0, ClientRectangle.Height / 2);
      highlightAlphaTop = 0;
      highlightAlphaBottom = 200;
      Invalidate();
      base.OnMouseDown(mevent);
    }

    protected override void OnMouseUp(MouseEventArgs mevent) {
      highlightRect.Location = new Point(0, 0);
      highlightAlphaTop = 255;
      highlightAlphaBottom = 0;
      if (DisplayRectangle.Contains(mevent.Location)) {
        animateMouseOverTimer.Start();
      }
      base.OnMouseUp(mevent);
    }

    protected override void OnMouseMove(MouseEventArgs mevent) {
      if ((mevent.Button &amp; MouseButtons.Left) == MouseButtons.Left &amp;&amp; 
!ClientRectangle.Contains(mevent.Location)) {
        OnMouseUp(mevent);
      }
      base.OnMouseMove(mevent);
    }

    private void animateMouseOverTimer_Tick(object sender, EventArgs e) {
      if (increasingAlpha) {
        if (100 &lt;= highlightAlphaBottom) {
          increasingAlpha = false;
        } else {
          highlightAlphaBottom += 5;
        }
      } else {
        if (0 &gt;= highlightAlphaBottom) {
          increasingAlpha = true;
        } else {
          highlightAlphaBottom -= 5;
        }
      }
      Invalidate();
    }

    private void animateResumeNormalTimer_Tick(object sender, EventArgs e) {
      bool modified = false;
      if (highlightAlphaBottom &gt; 0) {
        highlightAlphaBottom -= 5;
        modified = true;
      }
      if (highlightAlphaTop &lt; 255) {
        highlightAlphaTop += 5;
        modified = true;
      }
      if (!modified) {
        animateResumeNormalTimer.Stop();
      }
      Invalidate();
    }

  }
}
&lt;/pre&gt;
&lt;p&gt;So, in the end we have a gel button class that looks interesting (again, depending on your tastes), and actually exhibits pliancy to the user. It looks and behaves like a button - something that you can click.&lt;/p&gt;
&lt;p&gt;Of course, you already have that behavior in the default buttons, so this example is really only useful if you want to create a button to specifically support a design theme for your application. Otherwise, visual styles in Windows XP and above will already provide you with this functionality, and you don't need to write any code to achieve it.&lt;/p&gt;
&lt;p&gt;Windows, however, does not provide a control for every possible scenario. While you could use the base toolset to create just about any UI, you may very well want to optimize your UI by using a UI control that isn't provided in the base toolkit, and you don't want to have to compromise to fit your needs into this toolset. That's where custom controls become really interesting, and it is where we will go next.&lt;/p&gt;&lt;img src="http://blogs.msdn.com/aggbug.aspx?PostID=541384" width="1" height="1"&gt;</description><category domain="http://blogs.msdn.com/cjacks/archive/tags/User+Experience/default.aspx">User Experience</category></item></channel></rss>