My WordPress Blog

WordPress Blog

Chapter 2. Basic Brushes

The vast interior of the standard window is referred to as the window's client area. This is the part of the window in which your program displays text, graphics, and controls, and through which it receives user input.

The client areas of the windows created in the previous chapter were probably colored white, but that's only because white is the default color for the background of window client areas. You may have used Microsoft Windows Control Panel to set your system colors to non-default values for aesthetic reasons or to flaunt your eccentric individuality. More seriously, you might be someone who sees the screen better when the background of the window is black and foreground objects (such as text) are white. If so, you probably wish that more developers were aware of your needs and treated your desired screen colors with respect.

Color in the Windows Presentation Foundation is encapsulated in the Color structure defined in the System.Windows.Media namespace. As is customary with graphics environments, the Color structure uses levels of red, green, and blue primaries to represent color. These three primaries are generally referred to as R, G, and B, and the three-dimensional space defined by these three primaries is known as an RGB color space.

The Color structure contains three read/write properties of type byte named simply R, G, and B. The values of these three properties range from 0 through 255. When all three properties are 0, the color is black. When all three properties are 255, the color is white.

To these three primaries, the Color structure adds an alpha channel denoted by the property named A. The alpha channel governs the opacity of the color, where a value of 0 means that the color is entirely transparent and 255 means opaque, and values in between denote degrees of transparency.

Like all structures, Color has a parameterless constructor, but this constructor creates a color with the A, R, G, and B properties all set to 0a color that is both black and entirely transparent. To make this a visible color, your program can manually set the four Color properties, as shown in the following example:

Color clr = new Color(); clr.A = 255; clr.R = 255; clr.G = 0; clr.B = 255;

The resultant color is an opaque magenta.

The Color structure also includes several static methods that let you create Color objects with a single line of code. This method requires three arguments of type byte:

Color clr = Color.FromRgb(r, g, b)

The resultant color has an A value of 255. You can also specify the alpha value directly in this static method:

Color clr = Color.FromArgb(a, r, g, b)

The RGB color space implied by byte values of red, green, and blue primaries is sometimes known as the sRGB color space, where s stands for standard. The sRGB space formalizes common practices in displaying bitmapped images from scanners and digital cameras on computer monitors. When used to display colors on the video display, the values of the sRGB primaries are generally directly proportional to the voltages of the electrical signals sent from the video display board to the monitor.

However, sRGB is clearly inadequate for representing color on other output devices. For example, if a particular printer is capable of a greener green than a typical computer monitor, how can that level of green be represented when the maximum value of 255 represents the monitor green?

To meet these concerns, other RGB color spaces have been defined. The Color structure in the Windows Presentation Foundation supports one of these alternatives, the scRGB color space, which was formerly known as sRGB64 because the primaries are represented by 64-bit values. In the Color structure, the scRGB primaries are actually stored as single-precision float values. To accommodate the scRGB color space, the Color structure contains four properties of type float named ScA, ScR, ScG, and ScB. These properties are not independent of the A, R, G, and B properties. Changing the G property also changes the ScG property and vice versa.

When the G property is 0, the ScG property is also 0. When the G property is 255, the ScG property is 1. Within this range, the relationship is not linear, as shown in the following table.

scG

G

<= 0

0

0.1

89

0.2

124

0.3

149

0.4

170

0.5

188

0.6

203

0.7

218

0.8

231

0.9

243

>= 1.0

255


The relationship between scR and Rand between scB and Bis the same as that between scG and G. The values of scG can be less than 0 or greater than 1 to accommodate colors that are beyond the gamut of the video display and the numerical range of sRGB.

Cathode ray tubes in common use today do not display light in a linear fashion. The light intensity (I) is related to the voltages (V) sent to the display in the following power relationship:

I = V?

where the gamma exponent is a value that is characteristic of the display and ambient light, but for commonly used monitors and viewing conditions is generally between 2.2 and 2.5. (The sRGB standard assumes 2.2.)

Human visual perception to light intensity is nonlinear as wellapproximately proportional to the light intensity to the 1/3 power. Fortunately, the nonlinearity of human perception and the nonlinearity of the CRT tend to offset each other, so that the sRGB primaries (which are proportional to the display voltages) are roughly perceptually linear. That is, an RGB value of 80-80-80 (in hexadecimal) roughly corresponds to what a person might categorize as "medium gray." This is part of what makes sRGB such a compelling standard.

The scRGB primaries, however, are designed to be linear in relationship to light intensity, so the relationship between scG and G is


where the exponent of 2.2 is the value of gamma assumed in the sRGB standard. Notice that this relationship is approximate. It is least accurate in the low values. The transparency channel has a simpler relationship:


You can create a Color object based on scRGB primaries using this static method:

Color clr = Color.FromScRgb(a, r, g, b);

The arguments are float values, and can be less than 0 or greater than 1.

System.Windows.Media also includes a class named Colors (notice the plural) that contains 141 static read-only properties whose names begin alphabetically with AliceBlue and AntiqueWhite and conclude with Yellow and YellowGreen. For example:

Color clr = Colors.PapayaWhip;

All but one of these color names are the same as those commonly supported by Web browsers. The exception is the Transparent property, which returns a Color value with an alpha value of 0. The other 140 properties in the Colors class return a Color object based on preset sRGB values with an alpha level of 255.

Your program can change the background color of the client area by setting the Background property, a property that Window inherits from Control. However, you don't set Background to a Color object; you set Background to a much more versatile object of type Brush.

Brushes are used so extensively in the WPF that they demand early attention in this book. Brush itself is actually an abstract class, as shown in the following class hierarchy:

Object

    DispatcherObject (abstract)

          DependencyObject

                Freezable (abstract)

                       Animatable (abstract)

                             Brush (abstract)

                                   GradientBrush (abstract)

                                         LinearGradientBrush

                                         RadialGradientBrush

                                   SolidColorBrush

                                   TileBrush (abstract)

                                          DrawingBrush

                                          ImageBrush

                                          VisualBrush

What you actually use to set the Background property of the Window object is an instance of one of the nonabstract classes that inherit from Brush. All Brush-related classes are part of the System.Windows.Media namespace. In this chapter I will discuss SolidColorBrush and the two classes that inherit from GradientBrush.

As the name may suggest, the simplest type of brush is SolidColorBrush, which is a brush based on a single color. In one of the later programs in Chapter 1, you can set the color of the client area after including a using directive for System.Windows.Media and putting code like the following in the constructor for the Window class:

Color clr = Color.FromRgb(0, 255, 255); SolidColorBrush brush = new SolidColorBrush(clr); Background = brush;

This causes the background to appear as the color cyan. Of course, you can do the whole thing in one line of code, like this:

Background = new SolidColorBrush(Color.FromRgb(0, 255, 255));

SolidColorBrush also has a parameterless constructor and a property named Color that lets you set or alter the color of the brush after the object has been created. For example:

SolidColorBrush brush = new SolidColorBrush(); brush.Color = Color.FromRgb(128, 0, 128);

The following program varies the background color of the client area based on the proximity of the mouse pointer to the window's center. This program includes a using directive for System.Windows.Media, as will most future programs in this book.

VaryTheBackground.cs

[View full width]//-------------------------------------------------- // VaryTheBackground.cs (c) 2006 by Charles Petzold //-------------------------------------------------- using System; using System.Windows; using System.Windows.Input; using System.Windows.Media; namespace Petzold.VaryTheBackground { public class VaryTheBackground : Window { SolidColorBrush brush = new SolidColorBrush(Colors.Black); [STAThread] public static void Main() { Application app = new Application(); app.Run(new VaryTheBackground()); } public VaryTheBackground() { Title = "Vary the Background"; Width = 384; Height = 384; Background = brush; } protected override void OnMouseMove (MouseEventArgs args) { double width = ActualWidth - 2 * SystemParameters .ResizeFrameVerticalBorderWidth; double height = ActualHeight - 2 * SystemParameters .ResizeFrameHorizontalBorderHeight - SystemParameters.CaptionHeight; Point ptMouse = args.GetPosition(this); Point ptCenter = new Point(width / 2, height / 2); Vector vectMouse = ptMouse - ptCenter; double angle = Math.Atan2(vectMouse.Y, vectMouse.X); Vector vectEllipse = new Vector(width / 2 * Math.Cos(angle), height / 2 * Math.Sin(angle)); Byte byLevel = (byte) (255 * (1 - Math .Min(1, vectMouse.Length / vectEllipse.Length))); Color clr = brush.Color; clr.R = clr.G = clr.B = byLevel; brush.Color = clr; } } }


As you move the mouse toward the center of the client area, the background changes to lighter shades of gray. The background becomes black when the mouse is beyond an imaginary ellipse that fills the client area.

All the action happens in the overridden OnMouseMove method, which is called whenever the mouse is moved over the program's client area. The method is a little complex for a couple reasons. The method must first calculate the size of the client area, but unless there's something actually in the client area, there's no good way to determine its size. The method begins by using the ActualWidth and ActualHeight properties of the window and then subtracting the dimensions of the sizing border and caption bar as obtained from static properties of the SystemParameters class.

The method obtains the mouse pointer's location by calling the GetPosition method in the MouseEventArgs class, and saves that Point object in ptMouse. This location is a certain distance from the center of the client area, which is the Point structure named ptCenter. The method then subtracts ptCenter from ptMouse. If you examine the documentation of the Point structure, you'll find that subtracting one Point from another results in an object of type Vector, which this method saves as vectMouse. Mathematically, a vector is a magnitude and a direction. The magnitude of vectMouse is the distance between ptCenter and ptMouse, and it's provided by the Length property of the Vector structure. The direction of a Vector object is provided by its X and Y properties, which represent a direction from the originthe point (0, 0)to the point (X, Y). In this particular case, vectMouse.X equals ptMouse.X minus ptCenter.X and similarly for Y.

The direction of a Vector object can also be represented as an angle. The Vector structure includes a static method named AngleBetween that calculates the angle between two Vector objects. The OnMouseMove method in the VaryTheBackground program shows a direct calculation of the angle of vectMouse based on the inverse tangent of the ratio of its Y and X properties. This angle is in radians measured clockwise from the horizontal axis. The method then uses that angle to calculate another Vector object that represents the distance from the center of the client area to a point on an ellipse that fills the client area. The level of gray is simply proportional to the ratio of the two vectors.

The OnMouseMove method obtains the Color object associated with the SolidColorBrush originally created as a field of the class, sets the three primaries to the gray level, and then sets the Color property of the brush to this new value.

That this program works at all may astonish you. Obviously somebody is redrawing the client area every time the brush changes, but it's all happening behind the scenes. This dynamic response is possible because Brush derives from the Freezable class, which implements an event named Changed. This event is fired whenever any changes are made to the Brush object, and this is how the background can be redrawn whenever a change occurs in the brush.

This Changed event and similar mechanisms are used extensively behind the scenes in the implementation of animation and other features in the Windows Presentation Foundation.

Just as the Colors class provides a collection of 141 static read-only properties with all the named colors, a class named Brushes (again, notice the plural) provides 141 static read-only properties with the same names as those in Colors but which return objects of type SolidColorBrush. Instead of setting the Background property like this:

Background = new SolidColorBrush(Colors.PaleGoldenrod);

you can use this:

Background = Brushes.PaleGoldenrod;

Although these two statements will certainly color your window background with the same color, there is a difference between the two approaches that reveals itself in a program like VaryTheBackground. In that program try replacing the following field definition:

SolidColorBrush brush = new SolidColorBrush(Colors.Black);

with

SolidColorBrush brush = Brushes.Black;

Recompile and run. Now you get an Invalid Operation Exception that states "Cannot set a property on object '#FF000000' because it is in a read-only state." The problem is the very last statement in the OnMouseMove method, which attempts to set the Color property of the brush. (The hexadecimal number quoted in the exception is the current value of the Color property.)

The SolidColorBrush objects returned from the Brushes class are in a frozen state, which means they can no longer be altered. Like the Changed event, freezing is implemented in the Freezable class, from which Brush inherits. If the CanFreeze property of a Freezable object is true, it's possible to call the Freeze method to render the object frozen and unchangeable. The IsFrozen property indicates this state by becoming true. Freezing objects can improve performance because they no longer need to be monitored for changes. A frozen Freezable object can also be shared across threads, while an unfrozen Freezable object cannot. Although you cannot unfreeze a frozen object, you can make an unfrozen copy of it. The following code will work as a field definition of VaryTheBackground:

SolidColorBrush brush = Brushes.Black.Clone();

If you'd like to see these 141 brushes rendered on the window's client area, the FlipThroughTheBrushes program lets you use the up and down arrow keys to flip through them.

FlipThroughTheBrushes.cs

[View full width]//--------- --------------------------------------------- // FlipThroughTheBrushes.cs (c) 2006 by Charles Petzold //------------------------------------------------ ------ using System; using System.Reflection; using System.Windows; using System.Windows.Input; using System.Windows.Media; namespace Petzold.FlipThroughTheBrushes { public class FlipThroughTheBrushes : Window { int index = 0; PropertyInfo[] props; [STAThread] public static void Main() { Application app = new Application(); app.Run(new FlipThroughTheBrushes()); } public FlipThroughTheBrushes() { props = typeof(Brushes).GetProperties (BindingFlags.Public | BindingFlags.Static); SetTitleAndBackground(); } protected override void OnKeyDown (KeyEventArgs args) { if (args.Key == Key.Down || args.Key == Key.Up) { index += args.Key == Key.Up ? 1 : props.Length - 1; index %= props.Length; SetTitleAndBackground(); } base.OnKeyDown(args); } void SetTitleAndBackground() { Title = "Flip Through the Brushes - " + props[index].Name; Background = (Brush) props[index] .GetValue(null, null); } } }


This program uses reflection to obtain the members of the Brushes class. The first line of the constructor uses the expression typeof(Brushes) to obtain an object of type Type. The Type class defines a method named GetProperties that returns an array of PropertyInfo objects, each one corresponding to one of the properties of the class. You'll notice that the program explicitly restricts itself to the public and static properties from the Brushes class using a BindingFlags argument to GetProperties. That restriction isn't necessary in this case because all the properties of Brushes are public and static, but it can't hurt.

Both in the constructor and in the OnKeyDown override, the program calls SetTitleAndBackground to set the Title property and the Background property to one of the members of the Brushes class. The expression props[0].Name returns a string with the name of the first property in the class, which is the string "AliceBlue". The expression props[0].GetValue(null, null) returns the actual SolidColorBrush object. GetValue requires two null arguments for this job. Normally, the first argument would be the object you're obtaining the property value from. Since Brushes is a static property, there is no object. The second argument is used only if the property is an indexer.

The System.Windows namespace has a SystemColors class that is similar to both Colors and Brushes in that it contains only static read-only properties that return Color values and SolidColorBrush objects. This class provides the current user's color preferences as stored in the Windows registry. SystemColors.WindowColor, for example, indicates the user's preference for the background of the client area, while SystemColors.WindowTextColor is the user's preferred color for text in the client area. SystemColors.WindowBrush and SystemColors.WindowTextBrush return SolidColorBrush objects created with these same colors. For most real-world applications, you should use these colors for most simple text and monochromatic graphics.

The brush objects returned from SystemColors are frozen. Your program can change this brush:

Brush brush = new SystemColorBrush(SystemColors.WindowColor);

but it cannot change this brush:

Brush brush = SystemColors.WindowBrush;

Only objects based on classes that derive from Freezable can be frozen. There is no such thing as a frozen Color object, because Color is a structure.

One alternative to a solid-color brush is a gradient brush, which displays a gradually changing mix of two or more colors. Normally, gradient brushes would be an advanced programming topic, but they are easy to create in the Windows Presentation Foundation, and they are quite popular in modern color schemes.

In its simplest form, the LinearGradientBrush requires two Color objects (let's call them clr1 and clr2) and two Point objects (pt1 and pt2). The point pt1 is colored with clr1, and pt2 is colored with clr2. The line connecting pt1 and pt2 is colored with a mix of clr1 and clr2, so that the midpoint is the average of clr1 and clr2. Every line perpendicular to the line connecting pt1 and pt2 is colored uniformly with a proportion of the two colors. I'll discuss shortly what happens on the other side of pt1 and pt2.

Now here's the really good news: Normally you would have to specify the two points in units of pixels or (in the case of the Windows Presentation Foundation) device-independent units, and if you wanted to apply a gradient to a window background you would have to re-specify the points whenever the window size changed.

The WPF gradient brush includes a feature that makes it unnecessary to recreate or modify the brush based on the size of the window. By default, you specify the two points relative to the surface that the gradient brush is coloring, where the surface is considered to be 1 unit wide and 1 unit high. The upper-left corner of the surface is the point (0, 0). The lower-right corner is the point (1, 1).

For example, if you want red at the upper-left corner of your client area and blue at the lower-right corner, and a linear gradient between them, use the following constructor, which lets you specify two colors and two points:

LinearGradientBrush brush = new LinearGradientBrush(Colors.Red, Colors.Blue, new Point(0, 0), new Point(1, 1));

Here's a program that does precisely that:

GradiateTheBrush.cs

[View full width]//------------------------------------------------- // GradiateTheBrush.cs (c) 2006 by Charles Petzold //------------------------------------------------- using System; using System.Windows; using System.Windows.Input; using System.Windows.Media; namespace Petzold.GradiateTheBrush { public class GradiateTheBrush : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new GradiateTheBrush()); } public GradiateTheBrush() { Title = "Gradiate the Brush"; LinearGradientBrush brush = new LinearGradientBrush(Colors.Red , Colors.Blue, new Point (0, 0), new Point(1, 1)); Background = brush; } } }


As you change the size of the client area, this gradient brush changes behind the scenes. Again, the Change event implemented by the Freezable class makes this possible.

Although it's often convenient to set points using this relative coordinate system, it isn't the only option. The GradientBrush class defines a MappingMode property that you set to a member of the BrushMappingMode enumeration. The only members are RelativeToBoundingBox, which is the default, and Absolute, which lets you use device-independent units.

In hexadecimal RGB terms, the color at the upper-left corner of the client area in GradiateTheBrush is FF-00-00 and the color at the lower right is 00-00-FF. You would expect the color midway between those two to be either 7F-00-7F or 80-00-80, depending solely on rounding, and that is certainly the case, because the default ColorInterpolationMode property is the enumeration value ColorInterpolationMode.SRgbLinearInterpolation. The alternative is ColorInterpolationMode.ScRgbLinearInterpolation, which causes the midway color to be the scRGB value 0.5-0-0.5, which is the sRGB value BC-00-BC.

If you just need to create a horizontal or vertical gradient, it's easier to use this constructor of LinearGradientBrush:

new LinearGradientBrush(clr1, clr2, angle);

Specify angle in degrees. A value of 0 is a horizontal gradient with clr1 on the left, equivalent to

new LinearGradientBrush(clr1, clr2, new Point(0, 0), new Point(1, 0));

A value of 90 creates a vertical gradient with clr1 on the top, equivalent to

new LinearGradientBrush(clr1, clr2, new Point(0, 0), new Point(0, 1));

Other angle values may be trickier to use. In the general case, the first point is always the origin, while the second point is computed like so:

new Point(cos(angle), sin(angle))

For an angle of 45 degrees, for example, the second point is approximately (0.707, 0.707). Keep in mind that this is relative to the client area, so if the client area isn't square (which is often the case), the line between the two points is not actually at 45 degrees. Also, a good chunk of the lower-right corner of the window is beyond this point. What happens there? By default, it's colored with the second color. This behavior is governed by the SpreadMethod property, which is set to a member of the GradientSpreadMethod enumeration. The default is Pad, which means that the color at the end is just continued as long as it's needed. Other possibilities are Reflect and Repeat. You might want to try the following code in GradiateTheBrush:

LinearGradientBrush brush = new LinearGradientBrush(Colors.Red, Colors.Blue, new Point(0, 0), new Point(0.25, 0.25)); brush.SpreadMethod = GradientSpreadMethod.Reflect;

This brush displays a gradient from red to blue between the points (0, 0) and (0.25, 0.25), then from blue to red between (0.25, 0.25) and (0.5, 0.5), then red to blue from (0.5, 0.5) to (0.75, 0.75), and finally blue to red from (0.75, 0.75) to (1, 1).

If you make the window very narrow or very short to exaggerate the difference between the horizontal and vertical dimensions, the uniformly colored lines become nearly vertical or nearly horizontal. You may prefer that a gradient between two opposite corners have a uniformly colored line between the other two corners.

Will a diagram help? Here's the GradiateTheBrush client area when stretched to oblong dimensions:

The dashed lines represent the uniformly colored areas, which are always perpendicular to the line connecting pt1 and pt2. You might prefer a gradient that instead looks like this:

Now the area colored with magenta extends between the two corners. The problem is that we need to calculate pt1 and pt2 so that the line connecting those two points is perpendicular to the line connecting the bottom-left and top-right corners.

It can be shown (my passive-voice inner mathematician says) that the length of the lines from the center of the rectangle to pt1 and pt2 (which I'll call L) can be calculated like this:


where W is the width of the window and H is the height. Let me convince you. Here's the same view of the client area with some additional lines and labels:

Notice that the line labeled L is parallel to the line connecting pt1 and pt2. The sine of the angle a can be calculated in two ways. First, it's H divided by the length of the diagonal of the rectangle:


Or, W can be a hypotenuse if L is the opposite side:


Combine these two equations and solve for L.

The following program creates a LinearGradientBrush with a MappingMode of Absolute in its constructor, but not with the intention of using that brush without modification. The constructor also installs a handler for the SizeChanged event, which is triggered whenever the size of the window changes.

AdjustTheGradient.cs

[View full width]//-------------------------------------------------- // AdjustTheGradient.cs (c) 2006 by Charles Petzold //-------------------------------------------------- using System; using System.Windows; using System.Windows.Input; using System.Windows.Media; namespace Petzold.AdjustTheGradient { class AdjustTheGradient: Window { LinearGradientBrush brush; [STAThread] public static void Main() { Application app = new Application(); app.Run(new AdjustTheGradient()); } public AdjustTheGradient() { Title = "Adjust the Gradient"; SizeChanged += WindowOnSizeChanged; brush = new LinearGradientBrush(Colors .Red, Colors.Blue, 0); brush.MappingMode = BrushMappingMode .Absolute; Background = brush; } void WindowOnSizeChanged(object sender, SizeChangedEventArgs args) { double width = ActualWidth - 2 * SystemParameters .ResizeFrameVerticalBorderWidth; double height = ActualHeight - 2 * SystemParameters .ResizeFrameHorizontalBorderHeight - SystemParameters.CaptionHeight; Point ptCenter = new Point(width / 2, height / 2); Vector vectDiag = new Vector(width, -height); Vector vectPerp = new Vector(vectDiag .Y, -vectDiag.X); vectPerp.Normalize(); vectPerp *= width * height / vectDiag .Length; brush.StartPoint = ptCenter + vectPerp; brush.EndPoint = ptCenter - vectPerp; } } }


The event handler begins by calculating the width and height of the client area, just as in the VaryTheBackground program earlier in this chapter. The Vector object vectDiag is a vector representing the diagonal from the lower-left to the upper-right corner. This can alternatively be calculated by subtracting the coordinate of the lower-left corner from the upper-right corner:

vectDiag = new Point(width, 0) new Point(0, height);

The vectPerp object is perpendicular to the diagonal. Perpendicular vectors are easily created by swapping the X and Y properties and making one of them negative. The Normalize method divides the X and Y properties by the Length property so that the Length property becomes 1. The event handler then multiplies vectPerp by the length I referred to earlier as L.

The final step is to set the StartPoint and EndPoint properties of the LinearGradientBrush. These properties are normally set through one of the brush constructors, and they are the only two properties that LinearGradientBrush defines itself. (The brush also inherits some properties from the abstract GradientBrush class.)

Again, notice that it's only necessary for the program to change a property of the LinearGradientBrush for the window to update itself with the updated brush. That's the "magic" of the Changed event defined by the Freezable class (and similar WPF features).

The LinearGradientBrush is actually more versatile than the two programs presented so far. The brush can also create a gradient between multiple colors. To take advantage of this feature, it's necessary to make use of the GradientStops property defined by GradientBrush.

The GradientStops property is an object of type GradientStopCollection, which is a collection of GradientStop objects. GradientStop defines two properties named Color and Offset, and a constructor that includes these two properties:

new GradientStop(clr, offset)

The value of the Offset property is normally between 0 and 1 and represents a relative distance between StartPoint and EndPoint. For example, if StartPoint is (70, 50) and EndPoint is (150, 90), an Offset property of 0.25 refers to the point one-quarter of the distance from StartPoint to EndPoint, or (90, 60). Of course, if your StartPoint is (0, 0) and your EndPoint is (0, 1) or (1, 0) or (1, 1), the point corresponding to the Offset is much easier to determine.

Here's a program that creates a horizontal LinearGradientBrush and sets seven GradientStop objects corresponding to the seven traditional colors of the rainbow. Each GradientStop is 1/6 of the window width further to the right.

FollowTheRainbow.cs

[View full width]//------------------------------------------------- // FollowTheRainbow.cs (c) 2006 by Charles Petzold //------------------------------------------------- using System; using System.Windows; using System.Windows.Input; using System.Windows.Media; namespace Petzold.FollowTheRainbow { class FollowTheRainbow: Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new FollowTheRainbow()); } public FollowTheRainbow() { Title = "Follow the Rainbow"; LinearGradientBrush brush = new LinearGradientBrush(); brush.StartPoint = new Point(0, 0); brush.EndPoint = new Point(1, 0); Background = brush; // Rainbow mnemonic is the name Roy G. Biv. brush.GradientStops.Add(new GradientStop(Colors.Red, 0)); brush.GradientStops.Add(new GradientStop(Colors.Orange, .17)); brush.GradientStops.Add(new GradientStop(Colors.Yellow, .33)); brush.GradientStops.Add(new GradientStop(Colors.Green, .5)); brush.GradientStops.Add(new GradientStop(Colors.Blue, .67)); brush.GradientStops.Add(new GradientStop(Colors.Indigo, .84)); brush.GradientStops.Add(new GradientStop(Colors.Violet, 1)); } } }


From here, it's a short leap from the LinearGradientBrush to the RadialGradientBrush. All that's required is changing the name of the class used for the brush and removing the StartPoint and EndPoint assignments:

CircleTheRainbow.cs

[View full width]//------------------------------------------------- // CircleTheRainbow.cs (c) 2006 by Charles Petzold //------------------------------------------------- using System; using System.Windows; using System.Windows.Input; using System.Windows.Media; namespace Petzold.CircleTheRainbow { public class CircleTheRainbow : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new CircleTheRainbow()); } public CircleTheRainbow() { Title = "Circle the Rainbow"; RadialGradientBrush brush = new RadialGradientBrush(); Background = brush; // Rainbow mnemonic is the name Roy G. Biv. brush.GradientStops.Add(new GradientStop(Colors.Red, 0)); brush.GradientStops.Add(new GradientStop(Colors.Orange, .17)); brush.GradientStops.Add(new GradientStop(Colors.Yellow, .33)); brush.GradientStops.Add(new GradientStop(Colors.Green, .5)); brush.GradientStops.Add(new GradientStop(Colors.Blue, .67)); brush.GradientStops.Add(new GradientStop(Colors.Indigo, .84)); brush.GradientStops.Add(new GradientStop(Colors.Violet, 1)); } } }


Now the brush starts in the center of the client area with red, and then goes through the colors until violet defines an ellipse that fills the client area. Beyond the ellipse in the corners of the client area, violet continues because the default SpreadMethod is Fill.

Obviously, the RadialGradientBrush class defines several properties with useful default values. Three of these properties define an ellipse: The Center property is a Point object of default value (0.5, 0.5), which is the center of the area that the brush covers. The RadiusX and RadiusY properties are two double values that govern the horizontal and vertical radii of the ellipse. The default values are 0.5, so both horizontally and vertically the ellipse reaches the edges of the area filled by the brush.

The perimeter of the ellipse defined by the Center, RadiusX, and RadiusY properties is set to the color that has an Offset property of 1. (In CircleTheRainbow, that color is violet.)

A fourth property is named GradientOrigin, and like the Center property, it is a Point object with a default value of (0.5, 0.5). As the name implies, the GradientOrigin is the point at which the gradient begins. It is the point at which you'll see the color that has an Offset of 0. (In CircleTheRainbow, that's red.)

The gradient occurs between GradientOrigin and the circumference of the ellipse. If GradientOrigin equals Center (the default case), the gradient occurs from the center of the ellipse to its perimeter. If GradientOrigin is offset somewhat from Center, the gradient will be more compressed where GradientOrigin is closest to the ellipse perimeter, and more spread out where GradientOrigin is farther away. To see this effect, insert the following statement into CircleTheRainbow:

brush.GradientOrigin = new Point(0.75, 0.75);

You may want to experiment with the relationship between the Center and GradientOrigin properties; the ClickTheGradientCenter program lets you do so. It uses a two-argument constructor for RadialGradientBrush that defines the color at GradientOrigin and the color on the perimeter of the ellipse. However, the program sets the RadiusX and RadiusY properties to 0.10, and the SpreadMethod to Repeat so that the brush appears as a series of concentric gradient circles.

ClickTheGradientCenter.cs

[View full width]//--------- ---------------------------------------------- // ClickTheGradientCenter.cs (c) 2006 by Charles Petzold //------------------------------------------------ ------- using System; using System.Windows; using System.Windows.Input; using System.Windows.Media; namespace Petzold.ClickTheGradientCenter { class ClickTheRadientCenter : Window { RadialGradientBrush brush; [STAThread] public static void Main() { Application app = new Application(); app.Run(new ClickTheRadientCenter()); } public ClickTheRadientCenter() { Title = "Click the Gradient Center"; brush = new RadialGradientBrush(Colors .White, Colors.Red); brush.RadiusX = brush.RadiusY = 0.10; brush.SpreadMethod = GradientSpreadMethod.Repeat; Background = brush; } protected override void OnMouseDown (MouseButtonEventArgs args) { double width = ActualWidth - 2 * SystemParameters .ResizeFrameVerticalBorderWidth; double height = ActualHeight - 2 * SystemParameters .ResizeFrameHorizontalBorderHeight - SystemParameters.CaptionHeight; Point ptMouse = args.GetPosition(this); ptMouse.X /= width; ptMouse.Y /= height; if (args.ChangedButton == MouseButton .Left) { brush.Center = ptMouse; brush.GradientOrigin = ptMouse; } else if (args.ChangedButton == MouseButton.Right) brush.GradientOrigin = ptMouse; } } }


The program overrides OnMouseDown so that you can also click the client area. The left mouse button changes both the Center and GradientOrigin properties to the same value. You'll see that the whole brush is simply shifted from the center of the client area. A click of the right mouse button changes only the GradientOrigin. You'll probably want to keep fairly close to the Center point, and at least within the inner circle. Now you can see how the gradient is compressed on one side and expanded on the other.

The effect was so interesting that I decided to animate it. The following program, RotateTheGradientOrigin, does not use any of the animation features built into the Windows Presentation Foundation. Instead, it uses a simple timer to change the GradientOrigin property.

There are at least four timer classes in .NET, and three of them are named Timer. The Timer classes in System.Threading and System.Timers can't be used in this particular program because the timer events occur in a different thread, and Freezable objects must be changed in the same thread in which they're created. The Timer class in System.Windows.Forms is an encapsulation of the standard Windows timer, but using it would require adding the System.Windows.Forms.dll assembly as an additional reference.

The DispatcherTimer class located in the System.Windows.Threading namespace is the one to use in WPF programs if you need the events to occur in the application thread. You set an Interval property from a TimeSpan property, but you can't get more frequent "ticks" than once every 10 milliseconds.

The program creates a 4-inch-square window so that it doesn't eat up too much processing time.

RotateTheGradientOrigin.cs

[View full width]//--------- ----------------------------------------------- // RotateTheGradientOrigin.cs (c) 2006 by Charles Petzold //------------------------------------------------ -------- using System; using System.Windows; using System.Windows.Input; using System.Windows.Media; using System.Windows.Threading; namespace Petzold.RotateTheGradientOrigin { public class RotateTheGradientOrigin : Window { RadialGradientBrush brush; double angle; [STAThread] public static void Main() { Application app = new Application(); app.Run(new RotateTheGradientOrigin()); } public RotateTheGradientOrigin() { Title = "Rotate the Gradient Origin"; WindowStartupLocation = WindowStartupLocation.CenterScreen; Width = 384; // ie, 4 inches Height = 384; brush = new RadialGradientBrush(Colors .White, Colors.Blue); brush.Center = brush.GradientOrigin = new Point(0.5, 0.5); brush.RadiusX = brush.RadiusY = 0.10; brush.SpreadMethod = GradientSpreadMethod.Repeat; Background = brush; DispatcherTimer tmr = new DispatcherTimer(); tmr.Interval = TimeSpan .FromMilliseconds(100); tmr.Tick += TimerOnTick; tmr.Start(); } void TimerOnTick(object sender, EventArgs args) { Point pt = new Point(0.5 + 0.05 * Math .Cos(angle), 0.5 + 0.05 * Math .Sin(angle)); brush.GradientOrigin = pt; angle += Math.PI / 6; // ie, 30 degrees } } }


I've been focusing on the Background property of Window in this chapter, but three other properties of Window are also of type Brush. One of these is OpacityMask, a property that Window inherits from UIElement, but this is best discussed in context with bitmaps in Chapter 31.

Window inherits the other two Brush properties from Control. The first is BorderBrush, which draws a border around the perimeter of the client area. Try inserting this code in a recent program from this chapter:

BorderBrush = Brushes.SaddleBrown; BorderThickness = new Thickness(25, 50, 75, 100);

The Thickness structure has four properties named Left, Top, Right, and Bottom, and the four-argument constructor sets those properties in that order. These are device-independent units that indicate the width of the border on the four sides of the client area. If you want the same border width on all four sides, you can use the single-argument constructor:

BorderThickness = new Thickness(50);

Of course, you can use a gradient brush for the border:

BorderBrush = new GradientBrush(Colors.Red, Colors.Blue, new Point(0, 0), new Point(1, 1));

This looks a lot like a gradient brush that fills the client areayou'll see red at the top-left corner and blue at the bottom-right cornerexcept that the brush only appears around the perimeter of the client area. The BorderBrush detracts from the size of the client area, as is easy to determine when you include a BorderBrush and set the Background property with a gradient brush. If both BorderBrush and the Background have the same gradient brush, the two brushes don't blend in with each other:

Background = new GradientBrush(Colors.Red, Colors.Blue, new Point(0, 0), new Point(1, 1));

The background brush appears complete within the area not covered by the border brush.

The only other property of Window that is of type Brush is Foreground, but for that property to do anything, we need to put some content in the window. Content comes in many different forms, from plain text to graphical images to controls, and this is what you'll begin exploring in the next chapter.


Previous Page Next Page