Setting colour properties
Setting
If you have come here from the main article on colours, you will have seen how some colours, font colours for example, are represented in Word 2007, and not, perhaps, have been ready to make the leap straight to the code presented there; this page is for you. If you have come here from somewhere else, the background to this is presented in the main article and, unless you are familiar with colours in Word 2007, you are recommended to familiarise yourself with the early part of the article before reading further here.
Knowing how things are put together is all very well and good and it can certainly help in understanding, but sometimes you need something else. One such something else is the macro recorder and so, to try and make it easier to understand the codes used for colours, try recording some code. Open a document – any document – in Word 2007, select come text, and then use your own favourite way to record a macro. Open the Font Color Dialog (clicking the icon on the Home tab of the Ribbon is probably the easiest way) and click on the “Blue” icon under “Standard Colors”. Next reopen the Dialog and click on the “Blue, Accent 1” icon under “Theme Colors”. You may notice, incidentally, that standard blue and theme accent blue are different blues, despite the tips. Open the Dialog one last time and click on the “Blue, Accent 1, Lighter 40%” icon, again under “Theme Colors”. Stop recording and switch to the VBA Editor to see your code; it should look like this:
Sub Colours1() ' ' Colours1 Macro '
Selection.Font.Color = 12611584 Selection.Font.Color = -738131969 Selection.Font.Color = -738132071 End Sub
Your first thought is probably that that doesn't make anything clearer; the decimal numbers generated by the macro recorder serve only to muddy the waters. Well, yes, they do a little but this is just a step on the way. Press Ctrl+G to switch to the “Immediate Window”, type ?Hex(12611584) and press Enter. If you have typed — or copied and pasted — the number correctly, the answer you get should be c07000. Leading zeroes are omitted from hexadecimal numbers just as from any other number so, read as a code for four bytes, this is 0x00C07000. If you look back at the descriptions in the main article you will see that the only type of colour with 0x00 in the most significant byte is an RGB colour and you can then read the rest of the value as meaning blue 0xC0 (192), green 0x70 (112), red 0x00 (0) and, if you check, you will find that the standard colour named “Blue” is indeed RGB(0, 112, 192).
If you now repeat the procedure with the next number and type ?Hex(-738131969) in the Immediate Window, you will find that it is 0xD400FFFF. Looking back again, you will see that a value beginning 0xD is a new 2007-type scheme colour, in this case type 4 (Accent 1), and it has darkness and lightness unchanged. Note that you do not get the actual colour here at all; it is just a reference to an external definition.
Finally, -738132071 is 0xD400FF99, a reference to the same colour as the previous one (which you know), with darkness unchanged but with a lightness value of 0x99. Without giving you a maths lesson, I hope you can see that 0x99/0xFF is the same as 0x9/0xF, decimal 9/15, which is 6/10, or 60%. The way this works is that 40% lighter is held as 60% (=100 - 40).
The next thing to know is that VBA understands hex; a number prefixed with “&H” is understood to be a hexadecimal, rather than decimal, number, and now knowing the hexadecimal equivalents you can rewrite the recorded macro as:
Sub Colours1() ' ' Colours1 Macro '
Selection.Font.Color = &HC07000 Selection.Font.Color = &HD400FFFF Selection.Font.Color = &HD400FF99 End Sub
This makes three changes to the color of the Selection, because you made three changes when you were recording it. If you run the macro, all you will see in your document as a result of it will be the last change so it might as well be the only one you make. The macro can, therefore, be reduced to:
Sub Colours1() Selection.Font.Color = &HD400FF99 End Sub
Armed with this you can start to become a little dangerous. Using what you know so far you can change the hexadecimal value to produce a different percentage lighter or, perhaps, darker. Remember that the darkness won’t be changed unless the lightness is set to 0xFF. This will change the colour of the Selection to 25% darker than the Accent 1 colour:
Sub Colours1() Selection.Font.Color = &HD400C0FF End Sub
Select some text and run the macro. Keeping the text selected, open the Font Color Dialog and you will see the “Accent 1, 25% darker” icon highlighted to confirm this. In VBA, however, you are not restricted to the limited set of choices provided in the User Interface and you can use any percentage you like. Maybe you want your text just a little darker, not as much as 25%, say 5%, and perhaps you’d like to use Accent 2 instead of Accent 1:
Sub Colours1() Selection.Font.Color = &HD500F3FF End Sub
The choices are almost endless but you have to remember a lot of numbers and you have to hard code them. You may remember from the main article that colours are made available to VBA via the object model as Long data types and, although you might not have realised it, the numbers you have been entering so far have been Long values: the hex literals, just as decimal literals, are converted when the code is compiled. VBA is, however, cleverer than that and can usually deduce what you want and make the appropriate conversions behind the scenes at run time, even if you set the colour to a value of a different type, a string, say:
Sub Colours1() Selection.Font.Color = "&HD500FF99" End Sub
Although this will give you the correct result, it is generally better to be explicit about the conversion:
Sub Colours1() Selection.Font.Color = CLng("&HD500FF99") End Sub
This may not seem like a big change and, in some ways, it isn’t, but it allows for a much bigger change. Strings can be built up piece by piece, so this does exactly the same as the code above:
Sub Colours1() Selection.Font.Color = CLng("&H" & "D" & "5" & "00" & "FF99") End Sub
This is beginning to be adaptable but also, perhaps, slightly hard to read. "00", standing by itself, for example, doesn’t really convey any meaning; you, at the moment, know what it means but will anyone else, or will you in a month’s time? One way to help with this is to use names instead of literal values. Here is some more code that does the same as the above, again, but now it uses three Constants (declared with the Const keyword) for the parts that don’t change and two variables (declared with the Dim keyword) for the parts that do:
Sub Colours1() Const HexadecimalPrefix As String = "&H" Const UseThemeColor As String = "D" Const UnusedZeroByte As String = "00"
Dim ThemeColor As String Dim LightnessOrDarkness As String ThemeColor = "5" LightnessOrDarkness = "FF99" Selection.Font.Color = CLng(HexadecimalPrefix & _ UseThemeColor & _ ThemeColor & _ UnusedZeroByte & _ LightnessOrDarkness) End Sub
The code has got a little longer, and probably still leaves you somewhat underwhelmed, but if you now replace the "5" with a reference to the “Accent 2” colour from the Word enumeration constant, you will still get the same result but you should start to see how the process can become more flexible. Although, in the case of this particular value, it will work without further changes, in the general case you should convert the enumeration constant to a string representation of its hex value to be sure that you get the correct complete string for conversion to a Long (the value 10 (the hyperlink colour), for example, needs to end up as the string "A").
In order to keep the variable values distinct from operations on them, this is done here as a two-stage process, with an extra variable, ThemeColorIndex, which is defined as wdThemeColorIndex, the name of the enumeration and using it means that the VB Editor will prompt with the appropriate values when you are coding.
Sub Colours1() Const HexadecimalPrefix As String = "&H" Const UseThemeColor As String = "D" Const UnusedZeroByte As String = "00"
Dim ThemeColorIndex As WdThemeColorIndex Dim ThemeColor As String Dim LightnessOrDarkness As String ThemeColorIndex = wdThemeColorAccent2 ThemeColor = Hex$(ThemeColorIndex) LightnessOrDarkness = "FF99" Selection.Font.Color = CLng(HexadecimalPrefix & _ UseThemeColor & _ ThemeColor & _ UnusedZeroByte & _ LightnessOrDarkness) End Sub
Just as you can dynamically set the colour, you can also dynamically calculate the values required for the darkness and lightness percentages. Remember that 40% lighter was held as 60% of 0xFF; to calculate this you need (100-40), divided by 100 to convert the percentage into a value between 0 and 1, and then multiplied by 0xFF to convert again to a value between 0 and 0xFF. As with the Theme Colour value, the result then needs converting to the string representation of its hex value to be sure that you get the correct complete string, and finally, when setting a lightness, the darkness must be set to be unchanged.
As with the base colour, the setting of the value and the operations performed on it are kept separate by the use of a new variable, Amount, which is defined as a Double as it will need to hold non-integral values. This time, an extra constant is also defined for the constant 0xFF value that indicates that the darkness remains unchanged while the lightness is set.
Const Unchanged As String = "FF" Dim Amount As Double Amount = 40 LightnessOrDarkness = Unchanged & Hex$((100 - Amount) / 100 * &HFF)
There is one slight problem with this. Suppose that, instead of 40% lighter, you want to make your colour 98% lighter; you calculate (100-98)/100 to get 0.02, you then multiply that by 0xFF (255) to get 5.1 (which will be rounded to 5) and convert it to a hex string and you get "5". This is the correct result but you need two digits, you need "05", and arithmetical operations will not give you the leading zeroes. You must put a final tweak in there with a string operation to add a leading zero if needed:
Const Unchanged As String = "FF" Dim Amount As Double Amount = 40 LightnessOrDarkness = Unchanged & Right$("0" & Hex$((100 - Amount) / 100 * &HFF), 2)
Including this in the complete procedure, you get:
Sub Colours1() Const HexadecimalPrefix As String = "&H" Const UseThemeColor As String = "D" Const UnusedZeroByte As String = "00" Const Unchanged As String = "FF"
Dim ThemeColorIndex As wdThemeColorIndex Dim ThemeColor As String Dim LightnessOrDarkness As String Dim Amount As Double ThemeColorIndex = wdThemeColorAccent2 ThemeColor = Hex$(ThemeColorIndex) Amount = 40 LightnessOrDarkness = Unchanged & Right$("0" & Hex$((100 - Amount) / 100 * &HFF), 2) Selection.Font.Color = CLng(HexadecimalPrefix & _ UseThemeColor & _ ThemeColor & _ UnusedZeroByte & _ LightnessOrDarkness) End Sub
Now, I'm not sure that many people really understand percentages and computers certainly don't; computers do not work easily with percentages and generally only convert numbers to them at the user interface. Changing the way the LightnessOrDarkness string is built, to use a value between zero and one instead of the percentage simplifies it a little and requires only one compensating minor amendment elsewhere:
Sub Colours1() Const HexadecimalPrefix As String = "&H" Const UseThemeColor As String = "D" Const UnusedZeroByte As String = "00" Const Unchanged As String = "FF"
Dim ThemeColorIndex As wdThemeColorIndex Dim ThemeColor As String Dim LightnessOrDarkness As String Dim Amount As Double ThemeColorIndex = wdThemeColorAccent2 ThemeColor = Hex$(ThemeColorIndex) Amount = 40 / 100 LightnessOrDarkness = Unchanged & Right$("0" & Hex$((1 - Amount) * &HFF), 2) Selection.Font.Color = CLng(HexadecimalPrefix & _ UseThemeColor & _ ThemeColor & _ UnusedZeroByte & _ LightnessOrDarkness) End Sub
With this code, you can make a scheme colour lighter. No doubt, sooner or later, you’ll want to make one darker and, as you can probably imagine, the code for this is very similar. For reasons that will become clear in a moment, the amount of darkness is specified as a negative number instead of a positive one so, for example, to make the colour 40% darker, it takes a value of -40/100, or -0.4, instead of +40/100. Corresponding with this negative amount, the calculation becomes 1 + Amount, rather than 1 - Amount. The last thing to do is to swap the representations of the bytes round - the darkness goes in the first and the unchanged lightness in the second:
Sub Colours1() Const HexadecimalPrefix As String = "&H" Const UseThemeColor As String = "D" Const UnusedZeroByte As String = "00" Const Unchanged As String = "FF"
Dim ThemeColorIndex As wdThemeColorIndex Dim ThemeColor As String Dim LightnessOrDarkness As String Dim Amount As Double ThemeColorIndex = wdThemeColorAccent2 ThemeColor = Hex$(ThemeColorIndex) Amount = -0.4 LightnessOrDarkness = Right$("0" & Hex$((1 + Amount) * &HFF), 2) & Unchanged Selection.Font.Color = CLng(HexadecimalPrefix & _ UseThemeColor & _ ThemeColor & _ UnusedZeroByte & _ LightnessOrDarkness) End Sub
With the code as it stands you can change either lightness or darkness, but you will probably realise that it is just one or the other and you have to decide up-front which one and write the appropriate code. The two can, however, be combined without fear of conflict because, by using negative values for darkness you have created two mutually exclusive value ranges. If you take a look at the TintAndShade property of the ColorFormat object used to apply colour to drawing objects, you will see that it uses the same technique so this isn’t really breaking any new ground and, in deference to it, I have renamed the Amount variable TintAndShade. This code does exactly the same as the previous example, making the colour 40% darker but, by simply changing -0.4 to +0.4 it can now make the colour 40% lighter instead:
Sub Colours1() Const HexadecimalPrefix As String = "&H" Const UseThemeColor As String = "D" Const UnusedZeroByte As String = "00" Const Unchanged As String = "FF"
Dim ThemeColorIndex As wdThemeColorIndex Dim ThemeColor As String Dim LightnessOrDarkness As String Dim TintAndShade As Double ThemeColorIndex = wdThemeColorAccent2 ThemeColor = Hex$(ThemeColorIndex) TintAndShade = -0.4 If TintAndShade >= 0 Then LightnessOrDarkness = Unchanged & Right$("0" & Hex$((1 - TintAndShade) * &HFF), 2) Else LightnessOrDarkness = Right$("0" & Hex$((1 + TintAndShade) * &HFF), 2) & Unchanged End If Selection.Font.Color = CLng(HexadecimalPrefix & _ UseThemeColor & _ ThemeColor & _ UnusedZeroByte & _ LightnessOrDarkness) End Sub
Although this does practically all you want, you still have to go digging inside it to find and change the values for the theme colour and the lightness or darkness. As these are the only things you ever want to change it would be better if they were more obvious and accessible. Changing the procedure into a Function and making the two changeable values arguments of that Function, instead of variables declared within the procedure, will do exactly that.
Function GetThemeColor(ThemeColorIndex As wdThemeColorIndex, _
TintAndShade As Double) As Long
Const HexadecimalPrefix As String = "&H"
Const UseThemeColor As String = "D"
Const UnusedZeroByte As String = "00"
Const Unchanged As String = "FF"
' Dim ThemeColorIndex As wdThemeColorIndex ' To be removed Dim ThemeColor As String Dim LightnessOrDarkness As String ' Dim TintAndShade As Double ' To be removed ' ThemeColorIndex = wdThemeColorAccent2 ' To be removed ThemeColor = Hex$(ThemeColorIndex) ' TintAndShade = -0.4 ' To be removed If TintAndShade >= 0 Then LightnessOrDarkness = Unchanged & Right$("0" & Hex$((1 - TintAndShade) * &HFF), 2) Else LightnessOrDarkness = Right$("0" & Hex$((1 + TintAndShade) * &HFF), 2) & Unchanged End If GetThemeColor = CLng(HexadecimalPrefix & _ UseThemeColor & _ ThemeColor & _ UnusedZeroByte & _ LightnessOrDarkness) End Function
Now you have a function, and you can call it from anywhere, but it no longer sets the Font colour of the Selection – you must do that either after calling the function or as part of the calling statement, something like this:
Sub Colours1() Selection.Font.Color = GetThemeColor(ThemeColorIndex:=wdThemeColorAccent2, _ TintAndShade:=0.4) End Sub
All that remains is for some error checking to be added to the function in order to make it bullet-proof; this is left to the reader. One could, of course, further enhance the code in many ways, perhaps by defining a custom enumeration for the percentages; again, this is left for the reader who is bound only by his imagination.