Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Article Three: Building a UI Platform in C# - The Control Composite

0.00/5 (No votes)
16 Feb 2005 1  
Use TranslateTransform and Clip to effectively paint child controls.

Article Selector

Our first control

All of the controls we have been working with so far have been created for the sole purpose of developing the core infrastructure. That is, they are not intended for use in the production version of the platform. The simplicity of these testing controls allowed us to bring key areas of the platform into existence, using a step-by-step TDD approach. As we contemplate our first true control: Container, several issues quickly come to light.

Painting child controls

First, painting, as it is currently implemented, will not work for child controls inside a Container. In the Paint method of each control, at least two things are assumed:

  1. The control can start painting at 0,0.
  2. The control cannot paint outside the area defined by its size.

We are painting offscreen, to a bitmap that represents the client area of the form. To satisfy the first assumption then, we should call Graphics.TranslateTransform for each level of control in the composite. TranslateTransform moves the origin, putting any given child control in context. To satisfy the second assumption, we should set Graphics.Clip to the size of the control. Clip restricts the canvas to the size (or region) given. For example, assume we have a Form with a blue container in the ControlOverlay and a red container parented to the blue Container.

The painting process would proceed like this:

First paint the ControlOverlay, clipping to the client area of the form (shown here in white). Because the ControlOverlay is located at the origin (0,0) no translation is necessary.

Next, paint the blue container, clipping and translating to its size and location. Here we would translate to 10, 10 (the location of the blue container).

Finally paint the red container, clipping and again translating to 10, 10 (the location of the red container).

Well, it almost works this easily. The most important little technical gem here is that we must take into account the current clip when preparing the child control for painting. If we do not, only the child control bounds will be used and the child will paint as if it is completely visible (when in fact part or all of the child may be clipped by the parent). The code snippet for this bit:

Rectangle bounds = (Rectangle) ((IBounds) aControl).Bounds;

Rectangle intersection = Rectangle.Intersection(bounds, aGraphics.Clip);

if (!intersection.IsEmpty)
{
    aGraphics.Clip = intersection;
    aGraphics.TranslateTransform(bounds.Location);
    
    aControl.Paint(aGraphics);
}

Some reasonable tests for this painting scheme would be:

  1. Paint the blue container.
  2. Position the blue container so that it is clipped on the left.
  3. Position the blue container so that it is clipped on the top.
  4. Position the blue container so that it is clipped on the right.
  5. Position the blue container so that it is clipped on the bottom.
  6. Paint the blue container with a red child container.
  7. Position the red child container so that it is clipped on the left.
  8. Position the red child container so that it is clipped on the top.
  9. Position the red child container so that it is clipped on the right.
  10. Position the red child container so that it is clipped on the bottom.

After creating and mastering these tests, we are ready to move on to hit-testing.

Hit testing child controls

Because the testing controls introduced in previous articles resided in the ControlOverlay or DragOverlay (which represent the client area of the form), the bounds of these controls have always been relative to the form itself. This has made hit-testing quite simple. Container, with its ability to hold child controls, immediately breaks down this implementation. Why? Because the bounds of a control parented to a Container will be expressed in pixels relative to that Container, not relative to the form. Checking the bounds of these controls against a mouse point in form pixels, will not work. The diagram below shows the dilemma:

Here the blue container is located at 10, 10 and has a size of 100, 100. The red container is also located at 10, 10 (albeit inside the blue container) and has a size of 10, 10. Assuming we have a mouse move point of 25, 25, the HitTester should return the red container. However, the current implementation will return the blue container. Why? Because 25, 25 is outside the �native� bounds of the red container (10, 10, 20, 20). Before checking child controls, the HitTester must therefore reduce (or translate) the mouse move point based on the parent controls above it. In this case, before hit testing the red container, we must first reduce the mouse move point to 15, 15 (reducing it for the location of the blue container), then compare this new point with the bounds of the child control.

Some tests to prove out the new HitTester:

Parent Hot, Child Hot

Child Hot Clipped Left, Child Hot Clipped Top

Child Hot Clipped Right, Child Hot Clipped Bottom

In building these tests, we will be using Player-based cases. That is, the UI animation solution introduced in the previous article will now become our standard way of declaring and running tests, as this is now our most realistic and thorough testing method.

After implementing these classes and running the tests, we are green again:

Notice that these tests call for a change of color in the hot control. Here the blue container goes from navy to blue, and the red container goes from maroon to red. To get this kind of behavior, we will need a new kind of MouseTrap, the HotTrap:

  • ControlSystem - routes Windows.Form events to handling objects (such as the the Mouse).
  • HotRefresher - deploys a HotTrap into Mouse, paints the associated control on mouse enter or leave.
  • Mouse - handles all mouse related events.
  • MouseTrap - associated with a control, triggers events in response to certain mouse events (in this case OnMouseOver).
  • HotTrap - fires an event every time the mouse enters, hovers or leaves a control.
  • HitTester - finds the front-most control for a given point.
  • Control - base class for all controls.
  • Container - control which can contain other controls (like a panel).

To get a container to display as hot, we first have to associate the container with a HotRefresher:

Container container = new Container();

HotRefresher hotRefresher = new HotRefresher(container, ControlSystem);

HotRefresher deploys a HotTrap into ControlSystem.Mouse. When the HitTester returns the Container (i.e. the mouse has been positioned over the Container), the HotTrap is retrieved and its Move method is called. HotTrap sets the HotProperty of the control to true triggering an event. HotRefresher handles the event by refreshing the control, which then paints as hot. When the mouse leaves the control, the same process is used to paint the control cold.

Building the composite

The advent of the Container control reveals yet another area of the platform that could use work: the control composite. ControlOverlay currently sits at the base of the composite, and you can add controls to it like this:

Control anyControl = new Control();

Form.ControlSystem.ControlOverlay.Controls.Add(anyControl);

Here the Controls property is a ControlCollection. Exposing a collection as a property is fine, as long as you are willing to take on the burden of listening to the collection and responding to every change made to it. Coupled with this responsibility is the syntax itself, which seems foreign, especially if you are accustomed to parenting controls like this:

Control anyControl = new Control();

anyControl.Parent = Form.ControlSystem.ControlOverlay;

Not only is this syntax easier to follow, it isolates the handling of composite changes to one property setter (i.e. set_Parent).

Clearly the Parent property approach is the better of the two. For this reason we are going to remove the Controls property from ControlOverlay. Now this is going to break a lot of code in our tests, because almost every test involves creating a control and throwing it into the ControlOverlay or DragOverlay. Recoding the tests to use Parent instead of Controls.Add isn�t that big a deal. And guess what, the tests should still show up as green. The state of the controls at the end of each test should not be different, just because we built the composite differently. This is exactly what TDD is supposed to do: support us in refactoring.

Testing the composite is the same as testing any tree structure. We want to make sure that we add and remove both leaves and branches:

  1. Add a child to the ControlOverlay.
  2. Add a child to the ControlOverlay, then remove it.
  3. Add a child to a Container.
  4. Add a child to a Container, then remove it.
  5. Add a child with children to the ControlOverlay.
  6. Add a child with children to the ControlOverlay, then remove it.

Animating these tests seems unnecessary, but it actually gives us some nice visual feedback as the composite changes.

With these tests complete our control composite is in order, we�ve got better painting techniques, a better hit-testing technique and are ready to pursue another control: Label. That (among other things) will be the topic of the next article.

Project Stats

It is interesting that we are evenly balanced between test code and platform code at this point.

Downloads

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here