|
|
|
Coloring ContainersBy Joe Winchester |
|
The supplied container partsThe part AbtContainerDetailsView is placed on a composition editor and the collection of objects to be listed connected to its items attribute. For each column, an instance of the part AbtContainerDetailsColumn is placed onto the details view and the public interface attribute for the column's data is specified on the settings page. The background and foreground color can be set for the container details view as a whole by using the design time color chooser palette or else with the methods foregroundColor: aColorNameString and backgroundColor: aColorNameString. Both of these have the effect of changing the color of every cell in every column. In order to solve the problem of setting the color on a cell by cell basis the part must be looked at to see how it currently paints objects. The container parts buy most of their drawing functionality from underlying widgets that are part of the ExtendedWidgets framework. This is described as " ... classes and methods to enable application developers to expand upon graphical user interfaces" (ref 1). The area of interest is the one responsible for painting objects onto the screen. Wizardry with WidgetryIn the IBM Smalltalk common graphics subsystem, there is separation between the object being drawn, attribute information regarding how to draw the object, and the actual area onto which the drawing will occur. The attribute information is held in a graphics context, an instance of CgGC, which contains information such as colors, line widths, and line styles. The drawable area is a subinstance of CgDrawable and has the actual protocol to draw lines, arcs, rectangles, and other shapes onto an area of the screen. The extended widgets framework has been designed so that the type of visual object used is not restricted to any specific class, but rather has to conform to a specified protocol known as a renderable interface. This is made up of three methods:
With a container details column, each attribute of the item being listed is retrieved using the public interface getter method and turned into a string representation using a converter object ( ref 2 ). Each string is treated as a renderable object and drawn using the method EsString>>#ewDrawUsing: anEwRenderContext. An approach to solving the problem of coloring individual cells begins by creating a well-encapsulated object that is able to render strings in specific colors and substituting this into the cell. Having achieved this, there needs to be an unintrusive way to introduce this functionality onto a composition editor. Each of these problems is now tackled in turn. How to color a stringOne of the ways that I like to solve problems in Smalltalk is to start by thinking of a name for an object, then how it will be instantiated, and lastly how to implement it. To combine an object being rendered ( the string ) with information about how to render it ( its colors ), a suitable name might be ColoredString. This is a good name but I can envision a possible future requirement to store additional information about rendering the string, such as whether it is bold or in italics, as well as font name and pitch. A better name might be EmphasizedString. To instantiate the object, it is reasonable to extend the String class with the instance methods foregroundColor: aColorNameString and backgroundColorName: aColorNameString . These methods return an instance of the string together with its coloring attributes. To implement the class EmphasizedString, there are two options - first by inheritance and second by aggregation. Subclassing String appears to be a reasonable solution but I do not favor it for several reasons. From a technical standpoint, we would have to provide instance creation protocols that create a read stream over the String, a write stream over the EmphasizedString, and then painfully build the result character-by-character. From an implementation standpoint, subclassing String restricts us to only being able to store colors with a string, when in the future we might want to associate foreground and background colors with other renderable objects such as icons or even domain objects themselves. Thus I prefer to use aggregation over inheritance and extend the scope of our original object by renaming the class to EmphasizedRenderable. The instance of EmphasizedRenderable aggregates the parts of the string and directs all messages to the string except those concerned with the rendering protocol. This is an example of the Decorator Pattern ( ref 3 ) for which VisualAge provides a good abstract superclass named AbtObservableWrapper. Creating the wrapperAbtObservableWrapper has a single instance variable contents to which it redirects all but a few messages. By subclassing AbtObservableWrapper, we can add additional instance variables for the colors. AbtObservableWrapper subclass:#EmphasizedRenderable
instanceVariableNames: 'foregroundColor backgroundColor'
classVariableNames: ''
poolDictionaries: ''.
As described earlier, the desired instance creation protocol for the renderable wrapper is to allow the developer to simply set the foreground and background color of a string. String>>#foregroundColor:aColorNameString
^EmphasizedRenderable new
contents: self ;
foregroundColor: aColorNameString
String>>#backgroundColor:aColorNameString
^EmphasizedRenderable new
contents: self ;
backgroundColor: aColorNameString
Strictly speaking, we have not actually changed the color of the receiver, but instead returned a different object. We could actually turn the string into its decorated object as follows: String>>#foregroundColor:aColorNameString
self become: ( EmphasizedRenderable new
contents: self ;
foregroundColorName: aColorNameString )
The reason this is not at all advisable is twofold. First, the implementation of become: in IBM Smalltalk is slow, since it involves tracing a large number of object references in an image and can cause paging to occur. Second, the compiler and packager attempt to ensure that instances of String are unique in an image - meaning that even though the string 'Cancel' occurs fifteen times in an application, there might only be one object actually present. Making all instances become the same renderable object could have unwelcome side effects. The methods to set the colors of the emphasized renderable object also need to be written: EmphasizedRenderable>>#foregroundColor:aColorNameString
foregroundColor := aColorNameString
EmphasizedRenderable>>#backgroundColor:aColorNameString
backgroundColor := aColorNameString
To provide complete polymorphism we add these methods to Object itself: Object>>#foregoundColor:aColorNameString
^self convertToDisplay foregroundColor:aColorNameString
Object>>#backgroundColor:aColorNameString
^self convertToDisplay backgroundColor:aColorNameString
This provides the developer with the ability to set the color of any object that is going to be displayed in an extended widget, e.g. a Date or a Time. VisualAge provides a framework for converting any object to a string representation using subinstances of the class AbtConverter ( ref 4 ). The method Object>>#convertToDisplay uses a default converter class to provide conversion of any object to a displayable string. There is now a good interface that provides the ability to associate the foreground and background color with any object. The next thing to achieve is how to combine this with the extended widget's renderable protocol and get the EmphasizedRenderable object to draw its contents in the specified colors. Drawing in ColorThe method used to draw an object is ewDrawUsing: aResourceContext. The argument is an instance of EwRenderContext and its graphics context will determine the color to be used. This is made available on the public interface by the method setForeground: aColorIndex. The datatype of the argument is an integer representing a color index in a lookup table, whereas the instances of EmphasizedRenderable are holding their colors as strings such as 'red' or 'yellow'. There needs to be a conversion from one to the other. This morphing or casting of the string to its relevant color index involves first doing a conversion to an instance of the common graphics color class CgRGBColor. This is done by keying into the color database held in the default instance of CgScreen. The following code example will find the CgRGBColor instance for the VisualAge color 'red': CgScreen default lookupColor: 'red' The CgRGBColor object tells us the intensities of red, green, and blue that need to be combined to show the color we desire, but each display has its own particular color palette that corresponds to the characteristics of the actual device being used. The closest match of color on the palette needs to be found that best fits the CgRGCColor object. This is done with a method as follows: CgPalette>>#colorStringToPixelValue:aColorNameString
^aColorNameString == nil
ifTrue: [ 0 ]
ifFalse: [ self nearestPixelValue:
( CgScreen default lookupColor:aColorNameString ) ]
Now we have the protocol to convert VisualAge string colors into the integer value representing the index of the closest match in any given graphics palette. An instance of EwRenderObject aggregates the drawing area that contains the palette the item is going to be rendered onto, allowing us to write the method foregroundColor: aColorNameString as follows: EwRenderContext>>#foregroundColor:aColorNameString
self setForeground:
( self drawable getPalette colorStringToPixelValue: aColorNameString )
This method provides the necessary protocol to begin specializing EmphasizedRenderable to render actual instances: EmphasizedRenderable>>#ewDrawUsing:aRenderContext
"Store the current foreground color of the renderable context,
change it to be our color, render our contents, and set it back again"
| oldColor |
oldColor := aRenderContext getForeground.
aRenderContext foregroundColor:foregroundColorName.
self contents ewDrawUsing: aRenderContext.
aRenderContext setForegroundColor: oldColor
Background ColoringEwRenderContext has a method setBackground: aColorIndex and code could be written to set this to the background color of the EmphasizedRenderable object. However, after doing this the EmphasizedRenderable appeared unable to have any effect on the background color of the render context and the cell was always drawn with the background color of the container. Whether this is a bug or an undocumented feature I am not completely sure. After discussing this with a colleague, he suggested that even if I did succeed in changing the background color of the string being rendered, it would look strange to the user since only the area of the cell corresponding to the string's width would be rendered in the desired color. The best result would involve coloring in the entire cell's background into which the EmphasizedRenderable was being introduced. The EwRenderContext contains the information about the dimensions of the cell being rendered, the graphics context has the drawing attribute information, and the drawable has the protocol to actually draw onto the screen. This provides all the information needed to draw a rectangle over the current container column cell: EwRenderContext>>#fillCurrentRectangle
"Draw a rectangle corresponding to our current extent"
self drawable
fillRectangle: self gc
x: self x
y: self y
width: self width
height: self height
The rendering instance method on EmphasizedRenderable needs to be altered to draw a rectangle in the desired background color: EmphasizedRenderable>>#ewDrawUsing:aRenderContext
"Store the current foreground color of the renderable context,
change it to be our background color and fill a rectangle. Set the
color to be our foreground color, render our contents, and
finally restore the render context color back to its original value"
| oldColor |
oldColor := aRenderContext getForeground.
backgroundColor notNil ifTrue: [
aRenderContext
foregroundColor: backgroundColor ;
fillCurrentRectangle ;
foregroundColor: oldColor ].
foregroundColor notNil ifTrue: [
aRenderContext foregroundColor:foregroundColor ].
self contents ewDrawUsing: aRenderContext.
aRenderContext setForeground: oldColor
Double dispatchWith any method I write, I always look to see if there is a way to improve it. The thing I find important is good polymorphism across differing object types. In the above method, the fact that the message foregroundColor: is used to set the render context's foreground color to a string, and the message setForeground: for an integer looks clumsy. It should be possible to get one method to accept either class of argument and act accordingly. A first attempt at redesigning this code is as follows: EwRenderContext>>#foregroundColor:aColorNameStringOrInteger
aColorNameStringOrInteger class == String
ifTrue: [
self setForeground:
( self drawable getPalette
colorStringToPixelValue:aColorNameStringOrInteger ) ]
aColorNameStringOrInteger class == Integer
ifTrue: [ self setForeground:aColorNameStringOrInteger ].
The problem with this type of case logic in a method is that it is not extensible to new classes. If in the future there was a requirement to pass an instance of CgRGCColor, a third case block would need to be added. Good object-oriented code should allow new objects to be introduced in an existing design just by specifying an interface on the new objects, rather than cracking open the internals of the design itself. The power of this strategy can be seen by the fact that IBM followed this design when implementing renderables, thus allowing us to introduce EmphasizedRenderable just by making it conform to a given interface and not requiring changes to any widget componentry code. The technique to use in this type of problem is the double dispatch pattern ( ref 5 ) or pas-de-deux ( ref 6 ). This turns a message around and asks the argument to execute the functionality, allowing different classes of argument to specialize the double dispatched method appropriately. EwRenderContext>>#foregroundColor:aColor
"Double dispatch this back to the
argument"
aColor == nil
ifFalse: [ aColor setForegroundColorFor:self ]
The double dispatched method needs to be specialized on Integer and String, the two classes of argument: Integer>>#setForegroundColorFor:aRenderContext
aRenderContext setForeground: self
On String, we can convert ourselves to an integer and delegate processing to the same method: String>>#setForegroundColorFor:aRenderContext
( self drawable getPalette colorStringToPixelValue: aColorNameString )
setForegroundColorFor: aRenderContext
If the requirement arose to add an argument datatype of CgRGBColor, we would just have to extend its protocol to understand setForegroundColorFor: aRenderContext. The use of the double dispatch now allows the last line of the method EmphasizedRenderable>>#ewDrawUsing: aRenderContext to be changed from aRenderContext setForeground: oldColor to aRenderContext foregroundColor: oldColor Introducing emphasized renderables onto the VisualAge composition editorHaving created the EmphasisedRenderable class, there needs to be a good way to introduce it into the VisualAge composition editor. Finding such a solution involves looking at an existing scenario involving a container details view and then iterating over several possible approaches. A Person exampleTypically, a collection of domain objects is connected to the items attribute of an AbtContainerDetailsView. For an example domain object, I shall introduce a class JrwPerson, which has public interface attributes of fullName, birthDate and gender. In this scenario, when listing people we want our columns drawn with different colors depending on the gender and age of the person. This is illustrated in Figure 1. All of the code is provided for download, with only key methods used in the article to illustrate relevant points. Figure 1
The first requirement is that a person's name be shown in blue if they are male and red otherwise. A possible solution could be to create a new attribute on the public interface of JrwPerson called genderCodedFullName with a getter method as follows: JrwPerson>>#genderCodedFullName
^self isMale
ifTrue: [ self fullName foregroundColor:'blue' ]
ifFalse: [ self fullName foregroundColor:'pink' ]
The column listing the attribute fullName would be changed to list the attribute genderCodedFullName and the desired result would be achieved. This approach works, but has problems associated with it. First, we have logic on the Person object to create strings in certain colors - clearly overstepping the bounds of view / model separation. The JrwPerson object should have domain specific business knowledge only and as little as possible to do with anything related to views. Placing view logic inside a model object is considered bad coding practice, as it couples behavior across application layers and prevents future extensibility. The second problem is that our solution works well only for coloring attributes that were originally strings. The attribute birthDate returns an instance of Date. To color in the birthDate, a method could be coded as follows: Person>>#genderCodedBirthDate
^self isMale
ifTrue: [ self birthDate foregroundColor:'blue' ]
ifFalse: [ self birthDate foregroundColor:'red' ]
The problem here is that the returned instance of EmphasizedRenderable is wrapping a string object representing the default display implementation of the birthDate. VisualAge surfaces converters onto visual parts, allowing them to be customized locally. The date converter will only work with dates and is unable to process an instance of EmphasizedRenderable, precluding us from customizing a Date converter on the birthDate column. A possible solution would be to actually have the EmphasizedRenderable wrapper the domain object itself, so that when a Date instance had its foreground color set it would return an EmphasizedRenderable whose contents were the date itself. The problem here is that the converter would either reject this as a valid input class ( it would fail the #acceptsAsObjectToDipslayInput: anObject method ), or even if a conversion were done a String instance would be the output and the colors stored inside the EmphasizedRenderable would have been lost. Having rejected the idea of adding attributes returning colored strings to the Person class, another solution needs to be found. A good way to tackle any solution concerning objects is to think of the interface first and implementation later. How should the programmer specify the background and foreground color of each cell ? Using a public interface event as a callbackAn analogy to the problem we are trying to solve is found with the event cellValueRequested on AbtContainerDetailsColumn. This event can be connected to a keyword method with a single argument that is an instance of EwCellValueCallbackData. The argument object contains information about the cell being displayed, including its current value and the item the row is for. Modifying the value of the callback data has the effect of actually changing the item that will be displayed inside the cell. A connection can be made from the cellValueRequested event of the fullName column to a method as follows: JrwAppBldrViewSubclass>>#fullNameCellValueRequested:anEwCellValueCallbackData
anEwCellValueCallbackData item isMale
ifTrue: [anEwCellValueCallbackData value:
( anEwCellValueCallbackData value foregroundColor: 'blue' ) ]
ifFalse: [ anEwCellValueCalllbackData value:
( anEwCellValueCallbackData value foregroundColor: 'red' ) ]
Unfortunately, the event cellValueRequested occurs before the conversion from domain object to string and not after. This means that for columns such as birthDate, which is populated with Date instances, the same problems as before will be encountered. A solution is to create a new event on the container details column called cellEmphasisRequested. This event can be raised after the conversion from domain object to displayable string and connected to a method as above. To introduce the new event, a subclass of the existing container details column can be made: AbtContainerDetailsColumn>>subclass:#JrwEmphasizedContainerDetailsColumn
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
The event cellEmphasisRequested can be added to the new part. This is shown in Figure 2. Figure 2
Public interface event page of the new container details column. The event cellEmphasisRequested has a parameter named callback, which has an argument class of EwCellValueCallbackData. A method should be written that will return details for objects that have registered interest in the new event: JrwEmphasizedContainerDetailsColumn>>#cellEmphasisHandlers
^self abePartiesInterestedInAnEvent:#cellEmphasisRequested
The remaining task is to callback these handlers at an appropriate point late in the drawing of the cell, when its contents are guaranteed to be strings. The superclass method AbtContainerDetailsView>>#cellValue: clientData: callData: is responsible for calling back the event handlers for cellValueRequested as well as performing the domain model to string conversion. Specializing this method on the subclass allows the cellEmphasisRequested clients to be called back with the converted displayable string: JrwEmphasizedContainerDetailsColumn>>#cellValue:aTableColumn clientData: clientData callData: anEwCellValueCallbackData
super
cellValue: aTableColumn
clientData: clientData
callData: anEwCellValueCallbackData.
"Call back the emphasis requestors that can manipulate the displayable string"
self cellEmphasisHandlers == nil
ifFalse: [ self
callHandlers: self cellEmphasisHandlers
with: anEwCellValueCallbackData ].
This can now be connected to a method such as JrwAppBldrViewSubclass>>#cellValueRequested: anEwCellValueCallbackData described earlier. Revisiting this method, it looks as though it could be improved slightly. The following code construct appears twice: anEwCellValueCallbackData value: ( anEwCellValueCallbackData value foregroundColor: aString ) Following good object oriented principles, we should be able to encapsulate this behavior into the callback receiver object itself: EwCellCallbackData>>#foregroundColor:aString
self value: ( self value foregroundColor:aString )
EwCellCallbackData>>#backgroundColor:aString
self value: ( self value backgroundColor:aString )
This allows the target method of the cellEmphasisRequestsed event to be written as follows: JrwAppBldrViewSubclass>>#fullNameCellValueRequested:anEwCellValueCallbackData
anEwCellValueCallbackData item isMale
ifTrue: [anEwCellValueCallbackData foregroundColor: 'blue' ]
ifFalse: [ anEwCellValueCalllbackData foregroundColor: 'red' ]
ConclusionThe original requirement has been satisfied to allow the developer to dynamically specify the foreground and background color of a container cell. This involved looking at the extended widgets framework and how objects are rendered, as well as iterating over a number of different ways to introduce a solution into VisualAge. To work with the new container details column, use the menu option Add Part and enter a part name of JrwEmphasizedContainerDetailsColumn where you would have previously selected the part AbtContainerDetailsView from the parts palette. I hope you found this article informative and that it provides encouragement to dig and explore the power of VisualAge. I welcome all feedback. Footnote for VisualAge 4.0This article was written for use in VisualAge for Smalltalk version 3.0. When I attempted to port it across to version 4.0, I found that IBM had already implemented a similar solution. In VisualAge 4.0, all one has to do is connect the cellValueRequested event to a keyword method and the argument object EwCellValueCallbackData has new instance variables of foregroundColor and backgroundColor. These need to be set to an instance of CgRGBColor rather than VisualAge color strings. Conversion between the two is described earlier in this article. IBM's solution works well, although there are still avenues to explore with the EmphasizedRenderable allowing it to store formatting information such as bold, italics, font pitch and font name. In a future article, I hope to show how to extend the class to support these attributes. References1 - IBM Smalltalk Programmer's Reference. IBM SC34-4493-02 2 - VisualAge for Smalltalk User's Reference. IBM SC34-4519-00 3 - Gamma et al, Design Patterns, Addison Wesley 1995, ISBN 0-201-63361-2 4 - Programmer's Guide to Building Parts for Fun and Profit. IBM SC34-4496-00 5 - Beck, K. Smalltalk Best Practice Patterns Prentice Hall 1997, ISBN 0-13-476904-X 6 - Liu, Chamond. Smalltalk Objects and Design Prentice Hall 1996, ISBN 0-13-268335-0 |
Enjoy the article? Subscribe to Eye on Objects!Joe Winchester has been working with VisualAge for Smalltalk since 1994. His area of interest is in client server applications to the IBM AS/400 and believes that building good applications is all about partitioning logic between implementation layers around a solid business model. To this end he eagerly awaits the day when the AS/400 might actually become an object server and Smalltalkers can focus all their energies on real problems, rather than ones created by technology mismatches. He currently works for Computec International Resources in Southern California and can be reached at JoeW@concentric.net or 103 276.233@compuserve.com." |
|
![]() |