Compact Framework: adapt application graphics to the current color scheme

Published on November 10, 2008

Compact Framework: adapt application graphics to the current color scheme

    Introduction


    As you know, on Windows Mobile devices there is the possibility of changing the color scheme. If the application does not use graphic elements, it is enough to use the set of colors provided by the SystemColors class so that the application matches the current scheme. Of the most commonly used, it makes sense to mention ActiveCaption, ActiveCaptionText, InactiveCaption, InactiveCaptionText, WindowText, etc. Also, do not forget about the SystemBrushes class, which presents brushes ready for work - there is no need to call constructors, etc.

    But what to do when there is a set of images that must match the current color scheme? Is it really possible to make a set of pictures for all the primary colors?

    This is not the best solution - with any changes in the base image, you would have to recreate all versions of the same image in different shades.

    So what remains? Obviously, you need to somehow transform the basic image "on the fly." It is known that the most important component of the color scheme is contained in the registry at HKLM \ Software \ Microsoft \ Color , in the DWORD variable BaseHue . In case the value is in the range from 0 to 255, then we have gradations of gray. From 256 to 510 is the main rainbow :) It was experimentally established that various topics often put “whatever” into this variable, i.e. a value significantly exceeding the range of 0..510. As a result, to get an honest BaseHue, we use the following function:

    private const String BASEHUE_PATH = "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Color";

    public static int GetBaseHue()
    {
      object baseHue = Registry.GetValue(BASEHUE_PATH, "BaseHue", 0);
      int bh = baseHue == null ? 0: (int)baseHue;
         
      if (bh < 255)
        return bh;
      else
        return (bh & 0xFF) + 255;
    }

    * This source code was highlighted with Source Code Highlighter.


    You can read about the real meaning of BaseHue here: HSL color space .

    In short, with a BaseHue value between 0 and 255, we have gray gradations, so saturation should be 0 (i.e., guaranteed grayscale image). In the case of the range from 256 to 510, saturation is already at our discretion, if desired. I'm happy with 255, i.e. maximum color image. Now I will explain, and here saturation.

    The thing is that the image is stored in the RGB model, and BaseHue has nothing to do with RGB. As a result, it turns out that there is a need to perform RGB -> HSL conversion in order to be able to "color" the base image, and then the reverse HSL -> RGB conversion in order to get real colors for pixels.

    Application of the button with a checkbox as an example


    So, we will analyze the sequence of actions using the example of a graphic button with a check-box and make an active button out of an inactive button, and it will be in harmony with the current color scheme. Take a pre-prepared image of an inactive button. I note that the button has transparent zones, they are magenta colors, they can be seen in the corners.

    Original button
    Fig. 1 Original

    The first transformation - just make the whole button go to the specified BaseHue (in my case, 391).

    Button - hue applied
    Fig.2. Conversion is

    done. Here's a bad luck, the corner pixels responsible for transparency also changed color! Let’s go through the resulting image and restore justice (by sorting through the original and finding transparent pixels there): Fig .

    Button - hue applied, transparency restored
    3 “Transparent” pixels are restored

    Yes, transparency is now all right, but the checkmark and box underneath it hurt too ugly. Let's add more justice:

    Button - hue applied, transparency restored, original check restored
    Fig . 4. Check-box area restored.

    This passage, perhaps, is not as simple as the previous one. How was this done?

    If you look closely, it is quite obvious that the button image, free from the checkbox, in general, is repeated (except for angular fillets). And exactly the same substrate is under the check box. What is the conclusion? We have the opportunity to compare the “empty” background with the part where the check box is on top of this background, and if the discrepancy in R, G or B is more than a certain constant (by a simple search I got the number 25), then in the colored picture You can replace a pixel with a pixel from the original.

    And here is an example of what if you try not to use the threshold, but cut the original “on the forehead”: Fig .
    Button - hue applied, transparency restored, original check restored w / o level
    5 The check-box area is restored without taking into account the threshold

    Some code


    Now about the intricacies of implementation. In the Compact Framework there is not a word about RGB <-> HSL. Googling quickly enough solved the issue with conversions - RGB <-> HSL . But it did not immediately resolve the issue with the conversion rate. As you know, managed code is not fast when working with graphics, because GetPixel is terribly slow. But for this, a solution was found. An excellent post about UnsafeBitmap for operational pixel manipulations was found on the Windows Mobile MSDN blog .

    Below is a function that quickly enough paints an image at the specified hue, saturation, brigtness, using UnsafeBitmap :

    public static Bitmap ApplyHueSaturation(Bitmap input, int hue, int sat, int brightness)
    {
      if (input == null)
        return null;

      ColorHandler.RGB rgb;
      ColorHandler.HSV hsv;
      UnsafeBitmap ibmp = new UnsafeBitmap(input);
      UnsafeBitmap obmp = new UnsafeBitmap(new Bitmap(input.Width, input.Height));

      ibmp.LockBitmap();
      obmp.LockBitmap();

      for (int y = 0; y < input.Height; y++)
      {
        for (int x = 0; x < input.Width; x++)
        {
          UnsafeBitmap.PixelData c = ibmp.GetPixel(x, y);
          rgb.Red = c.red;
          rgb.Blue = c.blue;
          rgb.Green = c.green;

          hsv = ColorHandler.RGBtoHSV(rgb);
          hsv.Hue = hue;
          hsv.Saturation = sat;
          hsv.value += brightness;
          if (hsv.value > 255)
            hsv.value = 255;
          if (hsv.value < 0)
            hsv.value = 0;

          ColorHandler.RGB r = ColorHandler.HSVtoRGB(hsv);

          obmp.SetPixel(x, y, (byte)r.Red, (byte)r.Green, (byte)r.Blue);
        }
      }

      obmp.UnlockBitmap();
      ibmp.UnlockBitmap();

      return obmp.Bitmap;
    }


    * This source code was highlighted with Source Code Highlighter.

    A working example can be downloaded here .

    PS .: The speed of work in reality does not strike the imagination, however, I do the conversion only once, after which I boldly cache, the benefit of the small files are always obtained, even if it is a whole background for VGA resolution.

    PSS .: Yes, when transforming to optimize performance, of course, you can immediately ignore transparent pixels to avoid unnecessary enumeration of the image. Similar steps are shown for illustrative purposes only.

    UPD: It’s not enough to adapt the images to the current layout; you also need to display them correctly. Rounded edges can stay with purple corners! :) Read the continuation of the series about working with graphics in the Compact Framework.