More about buttons
Last time we ended up with a single button on the email composition window. It ought to be fairly obvious how to add additional similar buttons to that ribbon to, say, offer the ability to disable the other actions - just replicate the toggleButton XML fragment, change a few names, and implement differently named callback handlers. But that repetition of code ought to make your skin crawl just a little. If you've looked at some of the Office ribbon documentation, you may have noticed that toggleButton (and the other controls) have a tag attribute, and we can use this string pretty much as we please - for example, if we were to store the action index appropriate to each button there, we could use a common set of button handlers and have those determine the appropriate action based on the tag contents. The bold lines below show these tag settings in the ribbon XML:
<group id="grpReply" label="Response Options">
<toggleButton id="btnNoReplyAll"
imageMso="DistributionListRemoveMember"
screentip="No Reply All"
onAction="ComposeButton_Click"
getPressed="ComposeButton_IsPressed"
supertip="Prevent recipients within the same organisation from replying to all"
label="No Reply All"
tag="2"
size="large" />
<toggleButton id="btnNoForward"
imageMso="DistributionListUpdateMembers"
screentip="No Forward"
onAction="ComposeButton_Click"
getPressed="ComposeButton_IsPressed"
supertip="Prevent recipients within the same organisation from forwarding"
label="No Forward"
tag="3"
size="large" />
</group>
The callbacks handle the tags as in the following (again, the new stuff is in bold):
public bool ComposeButton_IsPressed(Office.IRibbonControl control)
{
bool pressed = false;
var inspector = control.Context as Outlook.Inspector;
int index = int.Parse(control.Tag);
if (inspector != null)
{
var item = inspector.CurrentItem as Outlook.MailItem;
if (item != null)
{
var action = items.Actions[index];
pressed = !action.Enabled;
Marshal.ReleaseComObject(action);
Marshal.ReleaseComObject(item);
}
Marshal.ReleaseComObject(inspector);
}
return pressed;
}
And similar for the click handler. Having added a second button, make sure that the ribbon invalidation code (triggered in This_Addin.OnNewInspector) invalidates the new one as well as the original.
In the latest version of the published add-in, I made the assumption that the reply-all button/action is the most important one, and the others are less likely to be put to use, so it's a bit wasteful to devote as much space for them on the ribbon. Toggle buttons are - obviously - not the only things you can put on the ribbon: I thought I'd see what a split button looks like. That's one with a main button and a drop down sub-menu into which other controls can be inserted. The following XML displays the no reply-all button as a large one on the ribbon, and no forward as a smaller button on its drop down.
<group id='grpReply' label='Response Options'>
<splitButton id='splNoReplyAll' size='large'>
<toggleButton id="btnNoReplyAll"
imageMso="DistributionListRemoveMember"
screentip="No Reply All"
onAction="ComposeButton_Click"
getPressed="ComposeButton_IsPressed"
supertip="Prevent recipients within the same organisation from replying to all"
label="No Reply All"
tag="2" />
<menu id='mnuResponseOptions' itemSize='normal'>
<toggleButton id="btnNoForward"
imageMso="DistributionListUpdateMembers"
screentip="No Forward"
onAction="ComposeButton_Click"
getPressed="ComposeButton_IsPressed"
supertip="Prevent recipients within the same organisation from forwarding"
label="No Forward"
tag="3" />
</menu>
</splitButton>
</group>
Take care if you're chopping and changing existing ribbon XML into this new structure: the controls embedded in the split button do not specify sizes, instead their sizes are taken from the attributes in parent the splitButton and menu elements.
This arrangement certainly does take up less screen space but it does have one drawback: only the state of the top level button is visible on the ribbon and you have to expand the menu to see the state of the sub-item, so I might revisit this for the next version I publish.
The next thing I want to look at is the images on the buttons. Instead of specifying imageMso you can set the image attribute, which lets you specify your own content instead of using what's in the Office resources. I'm not actually sure what the contents of that attribute string represent, but I do know that, as with many of the other attributes, there's a getImage callback which is quite easy to use. For example, create or find a 32x32 pixel image (that's the "large" image size; "normal" is 16x16) and add it to the project resources called, say, noreplyall, then define the callback method as shown below and specify it in one of the buttons' getImage attribute.
public System.Drawing.Bitmap ComposeButton_GetImage(Office.IRibbonControl control)
{
return Properties.Resources.noreplyall;
}
Previously we reduced code duplication by using the buttons' tag values to parameterize callback processing: we can do the same here. Add images as resources called, say, button1, button2, etc. ("button" followed by the tag values) and change the callback to:
public System.Drawing.Bitmap ComposeButton_GetImage(Office.IRibbonControl control)
{
return Properties.Resources.ResourceManager.GetObject("button" + control.Tag) as Bitmap;
}
It would be nice if that was all there was to it... But it's not. The getImage callback is passed no information about the size of the control so there's no way to tell whether to supply a 16x16 or 32x32 bitmap without some extra help. My pragmatic and not very clever solution is simply to have two image callbacks, one for each size, and connect the appropriate one to each control in the XML. Having said that, regardless of what size of image your callback supplies, it's scaled to fit the control on the screen - so, if you get it wrong, the button graphic will still look the right size, though it may have some scaling artefacts.
Actually, that's a bit of a lie: the above is indeed the case when your PC is running at the standard resolution of 96 dots per inch (100% in the Windows display control panel applet). Being a bit short-sighted, rather than squint at my screen, I've got my DPI set to 120 (represented as 125% in the Display applet). What I found was that a custom bitmap handed to getImage was scaled to fill a 40x40 space for a large icon (40 is 125% of 32) but the built in ribbon graphics were shown as 32x32 with extra space around them: hmmm. Similar, as you'd expect, happened to normal sized icons, with my carefully drawn 16x16 graphic being stretched horribly to fill 20x20, while the built in ones had extra padding. The technique I use now is to determine the current DPI setting: if it's such that the desired image size is smaller than or equal to what I have in resources, I just hand back my image and let the ribbon rendering code scale it as necessary - there's not much more I can do about it. If the desired image is larger than my bitmap, I create a new bitmap and draw mine in the centre of it. Something like:
private static Bitmap GetImage(string prefix, string tag, int targetSize)
{
int screenSize = GetScaledSize(targetSize);
var bitmap = Properties.Resources.ResourceManager.GetObject(prefix + tag + "_" + targetSize) as Bitmap;
if (targetSize < screenSize)
{
var scaledBitmap = new Bitmap(screenSize, screenSize);
using (var graphics = Graphics.FromImage(scaledBitmap))
{
int offset = (screenSize - targetSize) / 2;
graphics.DrawImage(bitmap, new Rectangle(offset, offset, bestSize, bestSize));
}
bitmap.Dispose();
bitmap = scaledBitmap;
}
return bitmap;
}
private static int GetScaledSize(int requestedSize)
{
return requestedSize * GetDPI() / 96;
}
My resources are stored as, say, "bitmap1_32" for the 32x32 no-reply-all button image and "bitmap2_16" for the 16x16 no-forward one, so I'd request the first of those via a call to GetImage("button", "1", 32). I'm not sure if there is a completely managed way to get hold of the current DPI setting, but the following P/Invoke-based code works just fine:
private static int GetDPI()
{
return GetDeviceCaps(88 /*LOGPIXELSX*/ );
}
private static int GetDeviceCaps(int index)
{
IntPtr hdc = GetDC(IntPtr.Zero); // TODO - ought to include some error handling, just in case...
int val = GetDeviceCaps(hdc, index);
ReleaseDC(IntPtr.Zero, hdc);
return val;
}
#region Imports
[DllImport("gdi32.dll")]
private static extern int GetDeviceCaps(IntPtr hdc, int index);
[DllImport("user32.dll")]
private extern static int ReleaseDC(IntPtr hwnd, IntPtr hdc);
[DllImport("user32.dll")]
private extern static IntPtr GetDC(IntPtr hwnd);
#endregion
Because I'm lazy, I've not experimented with higher (or lower) DPI settings, and what I have probably doesn't extrapolate further. (Perhaps if someone gives me a monitor with a stonkingly huge DPI, I'll be motivated to investigate!) I do note that at 150%, a large image would translate to 48x48 pixels. A 32x32 image sitting in the middle of all that space would look a little bit lost, and a look through the images stored within Outlook (open Outlook.exe in Visual Studio's resource editor and you'll see them all - but take care not to save any changes back to the executable!) shows that what look like 48x48 pixel versions of the ribbon images exist. Perhaps at such a high DPI setting, the ribbon will use 48x48 as large and 24x24 as small. If that guess is correct (and it would be easy for someone less lazy than me to check), one approach to providing the correct image would be to store 16x16, 24x24, 32x32 and 48x48 pixel images of all your buttons and use a combination of the DPI and large vs normal to work out what size is desired, then select the largest image size smaller than or equal to that from your collection, and use GetImage above to take care of DPI settings that fall between exact image sizes. As they say, I'll leave that as (yet another) exercise for the reader.