Chapter 18. The Decorator Pattern

The Decorator pattern provides us with a way to modify the behavior of individual objects without having to create a new derived class. Suppose we have a program that uses eight objects, but three of them need an additional feature. You could create a derived class for each of these objects, and in many cases this would be a perfectly acceptable solution. However, if each of these three objects requires different features, this would mean creating three derived classes. Further, if one of the classes has features of both of the other classes, you begin to create complexity that is both confusing and unnecessary.

For example, suppose we wanted to draw a special border around some of the buttons in a toolbar. If we created a new derived button class, this means that all of the buttons in this new class would always have this same new border when this might not be our intent.

Instead, we create a Decorator class that decorates the buttons. Then we derive any number of specific Decorators from the main Decorator class, each of which performs a specific kind of decoration. In order to decorate a button, the Decorator has to be an object derived from the visual environment so it can receive paint method calls and forward calls to other useful graphic methods to the object that it is decorating. This is another case where object containment is favored over object inheritance. The decorator is a graphical object, but it contains the object it is decorating. It may intercept some graphical method calls, perform some additional computation, and pass them on to the underlying ob ject it is decorating.

Decorating a CoolButton

Recent Windows applications such as Internet Explorer and Netscape Navi gator have a row of flat, unbordered buttons that highlight themselves with outline borders when you move your mouse over them. Some Windows programmers call this toolbar a CoolBar and the buttons CoolButtons. There is no analogous button behavior in VB controls, but we can obtain that behavior by decorating a PictureBox and using it as a button. In this case, we decorate it by drawing black and white border lines to highlight the button, or gray lines to remove the button borders.

Let's consider how to create this Decorator. Design Patterns suggests that Decorators should be derived from some general Visual Component class and then every message for the actual button should be forwarded from the decorator. In VB6, this is impractical because it is not possible to create a new visual control that contains an existing one. Further, even if we derived a control from an existing one, it would not have the line drawing methods we need to carry out decoration.

Design Patterns suggests that classes such as Decorator should be abstract classes and that you should derive all of your actual working (or concrete) decorators from the Abstract class. Here we show an Abstract class for a decorator that we can use to decorate picture boxes or other decorators.

'Class AbstractDecorator
'Used to decorate pictureBoxes
'and other Decorators
'--------
Public Sub init(c As Control, title As String)
'initializes decorator with control
End Sub
'--------
Public Sub initContents(d As AbstractDecorator)
'initializes decorator with another decorator
End Sub
'--------
Public Sub mouseUp()
End Sub
'--------
Public Sub mouseDown()
End Sub
'--------
Public Sub mouseMove(ByVal x As Single, ByVal y As Single)
End Sub
'--------
Public Sub refresh()
End Sub
'--------
Public Sub paint()
End Sub
'--------
Public Function getControl() As Control
End Function

Now, let's look at how we could implement a CoolButton. All we really need to do is to draw the white and black lines around the button area when it is highlighted and draw gray lines when it is not. When a MouseMove is detected over the button, it should draw the highlighted lines, and when the mouse leaves the button area, the lines should be drawn in gray.

However, VB does not have a MouseLeft event, so you cannot know for certain when the mouse is no longer over the button. As a first approximation, we detect the mouse crossing the outer 8 twips of the button area and treat that as an exit. To make sure that the button eventually un-highlights even if the mouse moves too quickly to trigger the exit criteria, we also use a timer to turn off the highlighting after 1 second if the mouse is no longer over the button.

Public Sub mouseMove(x As Single, y As Single)
Dim h As Integer, w As Integer, col As Long
h = pic.Height
w = pic.Width

If x < 8 Or y < 8 Or x > w - 16 Or y > h - 16 Then
    col = pic.BackColor
    drawLines True, col, False
    isOver = False
Else
    cTime = Time
    col = vbBlack
    drawLines False, col, False
    'isOver = True
End If

End Sub
'--------
Public Sub mouseDown()
  drawLines False, vbBlack, True
  isOver = True
End Sub
'--------
Public Sub mouseUp()
  isOver = False
  drawLines False, vbBlack, False
End Sub
'--------
Public Sub paint()
Dim x As Integer, y As Integer, h As Integer
  x = 10
  h = pic.Height
  y = 0.33 * h
  pic.PSet (x, y), pic.BackColor
  pic.Print btText;
End Sub
'--------
Private Sub drawLines(hide As Boolean, col As Long, _
   down As Boolean)
Dim h As Integer, w As Integer
h = pic.Height
w = pic.Width
If down Then
   col = vbBlack
    pic.Line (0, 0)-(w - 8, 0), col
    pic.Line -(w - 8, h - 8), col
    pic.Line -(0, h - 8), col
    pic.Line -(1, 1), col
Else
  If hide Then
    pic.Line (0, 0)-(w - 8, 0), col
    pic.Line -(w - 8, h - 8), col
    pic.Line -(0, h - 8), col
    pic.Line -(1, 1), col
  Else
    pic.Line (0, 0)-(w - 8, 0), vbWhite
    pic.Line -(w - 8, h - 8), col
    pic.Line -(0, h - 8), col
    pic.Line -(1, 1), vbWhite
  End If
End If

End Sub

We use a timer to see if it is time to repaint the button without highlights.

Public Sub tick()
Dim thisTime As Variant, diff As Variant
  thisTime = Time
  diff = DateDiff("s", cTime, thisTime)
  If diff >= 1 And Not isOver Then
    drawLines True, pic.BackColor, False
    isOver = False
  End If
End Sub

Using a Decorator

Now that we've written a CoolDecorator class, how do we use it? We simply put PictureBoxes on the VB Form, create an instance of the Decorator, and pass it the PictureBox it is to decorate. Let's consider a simple program with two CoolButtons and one ordinary Button. We create the buttons in the Form_Load event as follows.

Private Sub Form_Load()
cTime = Time    'get the time
Set deco = New Decorator
deco.init Picture1, "A Button"
deco.paint

Set deco2 = New Decorator
deco2.init picture2, "B Button"
deco2.paint

This program is shown in Figure 18-1, with the mouse hovering over one of the buttons.

The A button and B button are CoolButtons, which are outlined when a mouse hovers over them. Here the B button is outlined.

Figure 18-1. The A button and B button are CoolButtons, which are outlined when a mouse hovers over them. Here the B button is outlined.

Now that we see how a single decorator works, what about multiple decorators? It could be that we'd like to decorate our CoolButtons with another decoration—say, a diagonal red line. Since we have provided an alternate initializer with a Decorator as an argument, we can encapsulate one decorator inside another and paint additional decorations without ever having to change the original code. In fact, it is this containment and passing on of events that is the real crux of the Decorator pattern.

Let's consider the ReDecorator, which draws that diagonal red line. It draws the line and then passes control to the enclosed decorator to draw suitable Cool Button lines. Since Redecorator implants the AbstractDecorator interface, we can use it wherever we would have used the original decorator.

'Class Redecorator
'contains a Decorator which it further decorates
Implements AbstractDecorator
Private deco As AbstractDecorator
Private pic As PictureBox
'--------
Public Sub init(d As Decorator)
  Set deco = d
  Set pic = deco.getControl
End Sub
'--------
Private Function AbstractDecorator_getControl() As Control
 Set AbstractDecorator_getControl = pic
End Function
'--------
Private Sub AbstractDecorator_init(c As Control, title As String)
 'never called- included for completeness
End Sub
'--------
Private Sub AbstractDecorator_initContents(d As AbstractDecorator)
 init d
End Sub
'--------
Private Sub AbstractDecorator_mouseDown()
 deco.mouseDown
 AbstractDecorator_paint
End Sub
'--------
Private Sub AbstractDecorator_mouseMove(ByVal x As Single,_
                              ByVal y As Single)
 deco.mouseMove x, y
 AbstractDecorator_paint
End Sub
'-----
Private Sub AbstractDecorator_mouseUp()
 deco.mouseUp
End Sub
'--------
Private Sub AbstractDecorator_paint()
Dim w As Integer, h As Integer
 w = pic.Width
 h = pic.Height
 'draw diagonal red line
 pic.Line (0, 0)-(w, h), vbRed
 deco.paint 'and repaint contained decorator
End Sub
'--------
Private Sub AbstractDecorator_refresh()
 deco.refresh
 AbstractDecorator_paint
End Sub

You can create the CoolButton with these two decorators by just calling one and then the other during the Form_Load event.

Private Sub Form_Load()
  cTime = Time    'get the time
  'create first cool button
  Set deco = New Decorator
  deco.init Picture1, "A Button"
  deco.paint
  'create cool button
  Set deco2 = New Decorator
  deco2.init picture2, "B Button"
  'put it inside new decorator
  Set redec = New Redecorator
  redec.initContents deco2
  redec.paint
End Sub

This gives us a final program that displays the two buttons, as shown in Figure 18-2. The class diagram is shown in Figure 18-3.

The B button is also decorated with a SlashDecorator.

Figure 18-2. The B button is also decorated with a SlashDecorator.

The UML class diagram for Decorators and two specific Decorator implementations

Figure 18-3. The UML class diagram for Decorators and two specific Decorator implementations

Using ActiveX Controls as Decorators

The HiText control we created in Chapter 5 is an example of a control containing another control and operating on it. This is in fact a kind of decorator, too, and it is ideal for creating new derived controls. However, for simple things like borders, it is probably overkill.

A Decorator in VB.NET

We make a CoolButton in VB7 by deriving a container from the Panel class and putting the button inside it. Then, rather than subclassing the button (or any other control), we simply add handlers for the mouse and paint events of the button and carry out the operations in the panel class.

To create our panel decorator class, we create our form and then, using the VB7 designer IDE, use the menu items Project | Add User Control to create a new user control. Then, as before, we change the class from which the control inherits from UserControl to Panel.

Public Class DecoPanel
    Inherits System.WinForms.Panel

After compiling this simple and so-far empty class we can add it to the form, using the designer, and put a button inside it (see Figure 18-4).

The Design window for the Decorator panel

Figure 18-4. The Design window for the Decorator panel

Now the button is there, and we need to know its size and position so we can repaint it as needed. We could do this in the constructor, but this would break the IDE builder. Instead, we'll simply ask for the control the first time the OnPaint event occurs.

Protected Overrides Sub OnPaint _
                         ByVal e As System.Windows.Forms.PaintEventArgs)
        'This is where we find out about the control
        If Not gotControl Then  'once only
            'get the control
             c = CType(Me.Controls(0), Control)
            'set the panel size  1 pixel bigger all around
            Dim sz As Size
            sz.Width = c.Size.Width + 2
            sz.Height = c.Size.Height + 2
            Me.Size = sz
            'Me.Size.Width = c.Size.Width + 2
            'Me.Size.Height = c.Size.Height + 2
            x1 = c.Location.X - 1
            y1 = c.Location.Y - 1
            x2 = c.Size.Width
            y2 = c.Size.Height
            'create the overwrite pen
            gpen = New Pen(c.BackColor, 2)
            gotControl = True        'only once

Next we need to intercept the mouse events so we can tell if the mouse is over the button.

Dim evh As EventHandler = _
           New EventHandler(AddressOf ctMouseEnter)
  AddHandler c.MouseHover, evh
  AddHandler c.MouseEnter, evh
  AddHandler c.MouseMove, _
             New MouseEventHandler(AddressOf ctMouseMove)
  AddHandler c.MouseLeave, _
             New EventHandler(AddressOf ctMouseLeave)

The events these point to simply set a mouse_over flag to true or false and then repaint the control.

Public Sub ctMouseEnter(ByVal sender As Object, _
                  ByVal e As EventArgs)
        mouse_over = True
        Refresh()
    End Sub
    '-----
    Public Sub ctMouseLeave(ByVal sender As Object, _
                            ByVal e As EventArgs)
        mouse_over = False
        Refresh()
    End Sub
    '-----
    Public Sub ctMouseMove(ByVal sender As Object, _
                           ByVal e As MouseEventArgs)
        mouse_over = True
    End Sub

However, we don't want to just repaint the panel but to paint right over the button itself so we can change the style of its borders. We can do this by handling the paint event of the button itself. Note that we are adding an event handler and the button gets painted, and then this additional handler gets called.

'paint handler catches button's paint
 AddHandler c.Paint, _
            New PaintEventHandler(AddressOf ctPaint)

Our paint method draws the background (usually gray) color over the button's border and then draws the new border on top.

Public Sub ctPaint(ByVal sender As Object, _
 ByVal e As PaintEventArgs)
        'draw over button to change its outline
        Dim g As Graphics = e.Graphics
        'draw over everything in gray first
        g.DrawRectangle(gpen, 0, 0, x2, y2)
        'draw black and white boundaries
        'if the mouse is over
        If mouse_over Then
            g.DrawLine(bpen, 0, 0, x2 - 1, 0)
            g.DrawLine(bpen, 0, 0, 0, y2 - 1)
            g.DrawLine(wpen, 0, y2 - 1, x2 - 1, y2 - 1)
            g.DrawLine(wpen, x2 - 1, 0, x2 - 1, y2 - 1)
        End If
    End Sub

The resulting CoolButton is shown in Figure 18-5.

The CoolButton in VB.NET

Figure 18-5. The CoolButton in VB.NET

Using the general method of overriding panels and inserting controls in them, we can decorate any control to any length and can even redraw the face of the button if we want to. This sort of approach makes sense when you can't subclass the button itself because your program requires that it be of a particular class.

Nonvisual Decorators

Decorators, of course, are not limited to objects that enhance visual classes. You can add or modify the methods of any object in a similar fashion. In fact, non visual objects can be easier to decorate because there may be fewer methods to intercept and forward. Whenever you put an instance of a class inside another class and have the outer class operate on it, you are essentially “decorating” that inner class. This is one of the most common tools for programming available in Visual Basic.

Decorators, Adapters, and Composites

As noted in Design Patterns, there is an essential similarity among these classes that you may have recognized. Adapters also seem to “decorate” an existing class. However, their function is to change the interface of one or more classes to one that is more convenient for a particular program. Decorators add methods to particular instances of classes rather than to all of them. You could also imagine that a composite consisting of a single item is essentially a decorator. Once again, however, the intent is different.

Consequences of the Decorator Pattern

The Decorator pattern provides a more flexible way to add responsibilities to a class than by using inheritance, since it can add these responsibilities to selected instances of the class. It also allows you to customize a class without creating subclasses high in the inheritance hierarchy. Design Patterns points out two disadvantages of the Decorator pattern. One is that a Decorator and its enclosed component are not identical. Thus, tests for object types will fail. The second is that Decorators can lead to a system with “lots of little objects” that all look alike to the programmer trying to maintain the code. This can be a maintenance headache.

Decorator and Façade evoke similar images in building architecture, but in design pattern terminology, the Façade is a way of hiding a complex system inside a simpler interface, whereas Decorator adds function by wrapping a class. We'll take up the Façade next.

Thought Questions

Thought Questions
  1. When someone enters an incorrect value in a cell of a grid, you might want to change the color of the row to indicate the problem. Suggest how you could use a Decorator.

  2. A mutual fund is a collection of stocks. Each one consists of an array or Collection of prices over time. Can you see how a Decorator can be used to produce a report of stock performance for each stock and for the whole fund?

Programs on the CD-ROM

DecoratorCooldecorator VB6 cool button decorator
DecoratorRedecorator VB6 cool button and slash decorator
DecoratorDecoVBNet VB7 cool button decorator
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset