Today I'm delighted to present a new guest writer to the blog: Eric Faller, Software Design Engineer on the
Office User Experience Team.
Eric is one of the developers on our team who helped to design and implement RibbonX, the user interface extensibility model for Office developers.
Today's Guest Writer: Eric Faller
Some of the most commonly asked questions around RibbonX deal with how to load and get images to display properly in the Ribbon. This FAQ about images assumes that you're already familiar with writing RibbonX add-ins. If you're just getting started, check out the official documentation or the Developer category on this blog.
Alpha channels, masks, color keys, oh my!
The most significant change in Office 2007 is the switch to use images with alpha channels, instead of the masks or color keys used in previous Office releases. If that sentence sounds confusing, you're not alone. What are all these different technologies and what problem are they trying to solve?
The problem at hand is: how do we specify which parts of our images are transparent and should let the background color of the Ribbon show through? Before tackling that question, we should look at why it's even necessary to do this in the first place.
In the past, many add-in writers bypassed the entire issue by copying the background color of Office's UI and using that as the background color in their image. There are a couple of reasons this doesn't work very well. If the user switches their UI theme colors, or a new release of Office uses a different color, the icon suddenly looks out of place. For example, consider the following add-in image which looked good on gray toolbars but looks out of place in the Ribbon without transparency:
As we can see, in order for an add-in to look professional, it's going to need to pay attention to transparency. Let's look at what features previous releases of Office offered to solve this problem.
One way to solve this problem is to mark a certain color in the image as the "color key" and pretend that that color is transparent when drawing the image in the UI. The "hot pink" color is often used for this task since it doesn't occur very often in real icons:
The problems with this approach might be obvious. Back in the day when 16-color icons were the norm, an entire color had to be wasted as the transparent color. This color also had to be kept track of separately from the image itself: the image didn't contain the entire information needed to draw it properly. One way to work around this was to pick a specific pixel, such as the top-left corner, and use that as the color key, which led to the obvious problem of the top-left corner being unavailable for actual image content.
Picture & Mask
The CommandBars system solved the main problems with color keys by requiring add-ins to provide two different images to make up their icons: the "Picture" and "Mask" properties. The Picture image contained the color data of the icon, and the Mask was a black-and-white image that specified which pixels should be transparent. The Pictures and Masks were combined when drawing the final image on the UI. For example:
The most obvious drawback with this system is that you need to draw and keep track of two images per icon.
Another problem with both the Mask and Color Key systems is that they only allow for a single level of transparency: a pixel is either transparent or it's not. There's no room for a pixel to be "half-transparent." In today's world of rich, visual user interfaces, that's just not good enough anymore.
Alpha channels are a concept from computer graphics which involves adding another "channel" to each image to keep track of transparency information (along with the Red, Green and Blue channels). Each pixel contains a 4th "color" value which keeps track of how transparent it is, on a scale from completely opaque to completely transparent. When each individual pixel can have varying levels of transparency, it's possible to create nice smooth looking images.
For example, here's what our "A" image looks like when it's drawn in the Ribbon with an alpha channel:
The main problem with alpha channel-enabled images is that it's difficult to create them and make them look good. For example, Microsoft Paint does not support alpha channels so you will need to turn to professional-level software such as Photoshop (or free alternatives such as Paint.NET) to draw them.
Another hurdle to using alpha channels is that common file formats do not support them uniformly:
No (technically Yes, but most libraries
Single level only (no semi-transparency)
Yes - full support
PNG is the only common file format with full alpha channel support and widespread tool support. It's the recommended format for storing RibbonX images.
So, you might be wondering "Which file formats does RibbonX support?"
RibbonX operates on bitmap objects in code, not on files on the disk. It's the add-in which actually loads the files and returns the bitmap objects to RibbonX. Thus, an add-in can use whatever file format it wants when it is loading its images.
A better way to think about the question might be, "What file formats do the libraries I'm using support?" Unfortunately the answer can be a bit complicated because the libraries for different languages and technologies (VBA, C++, .NET, etc.) vary widely in their support for image file formats and alpha channels. GDI+ can be used from both native C++ code as well as managed C#/VB.NET code and it supports loading all common file formats, so it's the recommended library to use to load images.
If you're using VBA, you might think you're out of luck since the native VB APIs like LoadPicture() don't support PNG files, and you can't use GDI+ without mucking around with importing Win32 functions and types. Fortunately, VBA add-ins live in the new Open XML format files (except for in Access), and they can refer to files directly in those ZIP packages. RibbonX will do the work of automatically loading those files out of the packages, so most VBA code should not need to worry about loading image files. In this case RibbonX uses GDI+ to load those files, so any file format supported by GDI+ is supported in Open XML files (almost all formats). VBA add-ins will only need to load images themselves if they want to dynamically switch icons at runtime (this is not recommended by the UI guidelines since this almost never happens in the built-in Office UI.)
16-bit vs. 32-bit
Several things can go wrong in an add-in while it's loading its images before it passes them off to RibbonX. These usually involve the in-memory format used to store the image.
When storing an image in memory, how should the pixel data be represented? In the past, a wide variety of options were available (palletized, 4-bit, 16-bit, 24-bit, etc...), but today the most commonly encountered formats are 16 bits per pixel (bpp) and 32 bpp.
Remember that with alpha channels, each pixel stores its transparency value along with the color data. With 16-bit formats, 5 bits are usually allocated for each of the Red, Green and Blue channels (sometimes 6 bits for Green), leaving only 0 or 1 bits for alpha, which isn't enough. Thus, when an image is loaded into memory in 16-bit format, its alpha channel is usually compacted or deleted completely. Obviously this isn't what we want, so in our RibbonX add-ins, we need to always load images into memory in 32-bit formats, allocating 8 bits each for the Red, Green, Blue, and Alpha channels
So, if the file format you're using (PNG, for example) includes the full 32-bit pixel data, why would the image become compacted to only 16 bits when loading into memory? Unfortunately this happens more than one might expect.
DIB vs. DDB
Windows has a concept of "Device-Dependent Bitmaps" (DDBs) and "Device-Independent Bitmaps" (DIBs). The "device" referenced here is the graphics card and display on the computer. Today most displays can be set to either 16-bit mode or 32-bit mode. If your display is in 16-bit mode, DDBs will be in 16-bit format ("dependent" on the device), while DIBs will usually be 32-bit (since they are "independent" of the mode the device is actually in). If your display is in 32-bit mode, DDBs will be 32-bit and DIBs will usually be too, so there's no practical difference between DDBs and DIBs. Writing your code assuming there is no difference is often a cause of difficult bugs.
A commonly encountered problem is "My RibbonX icon looks fine on my computer, but on my tester's computer it looks horrible," or "It looks fine everywhere except over Remote Desktop, where it looks bad." In this case the tester's computer is probably running a 16-bit display. Remote Desktop and Terminal Services also default to 16-bit display modes in many cases.
The root of the problem is that the add-in is loading its images as DDBs, and on the 16-bit displays, the pixel data gets compacted to 16-bits and the alpha channel is thrown away. On 32-bit displays, the DDBs are equivalent to DIBs, and the pixels are loaded into 32 bits and everything works fine.
The fix is to make sure that your code always loads images as DIBs, never DDBs. Unfortunately for us, most Win32 image loading functions will create DDBs by default. This can be overridden by making sure to pass in LR_CREATEDIBSECTION to functions which take that flag, such as LoadImage(), CopyImage(), etc.. If you want to know whether you have a DIB or a DDB, you can call GetBitmap() on your HBITMAP with a DIBSECTION structure and test if that succeeds or fails.
Fortunately, if you are writing a .NET managed add-in and are using GDI+ to load your images, you don't have to worry about this because GDI+ uses DIBs internally.
IPictureDisp vs. System.Drawing.Bitmap
Once you have your image all loaded up, how do you return it to RibbonX? If you take a look at the return value of the "getImage" and "loadImage" functions, you'll see that it's a generic "object" type (or "IUnknown" in unmanaged code). The following types of values are accepted:
- IPictureDisp objects
- System.Drawing.Bitmap objects
- Strings (equal to the "imageMso" value of a built-in icon to use)
The 3rd option can be used if you want to re-use a built-in image. The first two require loading your own image and are more complicated.
The type of value you should return depends on the language you are using to write your add-in:
- VB6/VBA: IPictureDisp is the "COM name" of Picture, which is the native VB image type. You can just take the return value from the VB LoadPicture() function and return it (LoadPicture does not support PNG files, though, see the discussion above).
- C/C++ and other native code: IPictureDisp is your best bet. IPictureDisp objects are really just wrappers around HBITMAPs or HICONs, which are the native Win32 image types. You can either create IPictureDisps directly using functions like OleLoadPicture(), or indirectly by filling in a PICTDESC structure with an HBITMAP and calling OleCreatePictureIndirect() to convert it to an IPictureDisp. You can get the HBITMAP from any image-loading function you want, but GDI+ is the recommended library since it supports PNG files.
- C#/VB.NET and other managed code: System.Drawing.Bitmap is the native type of .NET languages, so it's easiest to just return Bitmap objects directly. If for some reason you want to convert them to IPictureDisp objects, you could do so using the AxHost.GetIPictureDispFromPicture() method.
Right now you might be wondering to yourself, "If Office is a native unmanaged application, how does it know what a managed System.Drawing.Bitmap object is?" This is a good question, and the answer is that it actually doesn't know. Through the magic of COM interop, the .NET Bitmap object is converted to a COM-compatible IDispatch interface which can be manipulated by Office. RibbonX takes this IDispatch object and checks if there is a "GetHbitmap()" function available on the interface. If there is, it uses Invoke() to call that function and get an HBITMAP object that corresponds to the original Bitmap object.
So in reality, RibbonX functions are not limited to returning System.Drawing.Bitmap objects, they can actually return any objects which have functions named "GetHbitmap." If for some reason you needed to, you could create your own class with a GetHbitmap() function and use that instead of System.Drawing.Bitmap.
Bitmaps vs. Icons
Some add-in writers store their UI icons in .ICO files and load them into HICON or System.Drawing.Icon objects.
RibbonX can load .ICO files from Open XML format files, and it will accept HICON-based IPictureDisps, but its icon support is somewhat limited and it doesn't accept System.Drawing.Icon objects at all.
Several RibbonX features do not work with icon-based images, such as the automatic "graying out" of images when their respective buttons are disabled, so it's recommended that add-ins provide bitmaps instead of icons. If you have a System.Drawing.Icon object, you can convert it to a Bitmap using the .ToBitmap() method.
getImage vs. loadImage
Another common question is "Why are there both getImage and loadImage functions? Which should I use?"
The answer is that you should almost always use loadImage, except when you need to dynamically change your controls' images at runtime, in which case you should use getImage on those controls.
loadImage provides these advantages over getImage:
- If multiple controls use the same image, loadImage is called only once, saving time and space.
- If a control is invalidated (for example, in order to change its enabled state), loadImage is not uselessly called again.
- Arbitrary strings can be specified in the "image" property and passed to loadImage (for example, a resource ID or filename), but with getImage you're stuck with the ID of the specific control you're interested in.
The main advantage of getImage is that its return value can be invalidated using IRibbonUI.InvalidateControl() and changed dynamically at runtime. Once an image is set with loadImage, it's permanent.
For more information, check out the official RibbonX documentation.
If you have specific support questions unrelated to this article, try the Office Discussion Groups.