Example Code
Suppose we want to draw a small folder icon with a name under it for each person in an organization. If this is a large organization, there could be a large number of such icons, but they are actually all the same graphical image. Even if we have two iconsone for "is Selected" and one for "not Selected"the number of different icons is small. In such a system, having an icon object for each person, with its own coordinates, name, and selected state, is a waste of resources. We show two such icons in Figure 19-2.
Figure 19-2. The Flyweight display with one folder selected
Instead, we'll create a FolderFactory that returns either the selected or the unselected folder drawing class but does not create additional instances once one of each has been created. Since this is such a simple case, we just create them both at the outset and then return one or the other.
public class FolderFactory { private Folder selFolder, unselFolder; //----- public FolderFactory() { //create the two folders selFolder = new Folder(Color.Brown); unselFolder = new Folder(Color.Bisque); } //----- public Folder getFolder(bool selected) { if(selected) return selFolder; else return unselFolder; } }
For cases where more instances could exist, the Factory could keep a table of those it had already created and only create new ones if they weren't already in the table.
The unique thing about using Flyweights, however, is that we pass the coordinates and the name to be drawn into the folder when we draw it. These coordinates are the extrinsic data that allow us to share the folder objects and, in this case, create only two instances. The complete folder class shown here simply creates a folder instance with one background color or the other and has a public draw method that draws the folder at the point you specify.
public class Folder { //Draws a folder at the specified coordinates private const int w = 50; private const int h = 30; private Pen blackPen, whitePen; private Pen grayPen; private SolidBrush backBrush, blackBrush; private Font fnt; //------ public Folder(Color col) { backBrush = new SolidBrush(col); blackBrush = new SolidBrush(Color.Black); blackPen = new Pen(Color.Black); whitePen = new Pen(Color.White); grayPen = new Pen(Color.Gray); fnt = new Font("Arial", 12); } //----- public void draw(Graphics g, int x, int y, string title) { //color folder g.FillRectangle(backBrush, x, y, w, h); //outline in black g.DrawRectangle(blackPen, x, y, w, h); //left 2 sides have white line g.DrawLine(whitePen, x + 1, y + 1, x + w - 1, y + 1); g.DrawLine(whitePen, x + 1, y, x + 1, y + h); //draw tab g.DrawRectangle(blackPen, x + 5, y - 5, 15, 5); g.FillRectangle(backBrush, x + 6, y - 4, 13, 6); //gray line on right and bottom g.DrawLine(grayPen, x, y + h - 1, x + w, y + h - 1); g.DrawLine(grayPen, x + w - 1, y, x + w - 1, y + h - 1); g.DrawString(title, fnt, blackBrush, x, y + h + 5); } }
To use a Flyweight class like this, your main program must calculate the position of each folder as part of its paint routine and then pass the coordinates to the folder instance. This is actually rather common, since you need a different layout, depending on the window's dimensions, and you would not want to have to keep telling each instance where its new location is going to be. Instead, we compute it dynamically during the paint routine.
Here we note that we could have generated an ArrayList of folders at the outset and simply scanned through the array to draw each folder. Such an array is not as wasteful as a series of different instances because it is actually an array of references to one of only two folder instances. However, since we want to display one folder as "selected," and we would like to be able to change which folder is selected dynamically, we just use the FolderFactory itself to give us the correct instance each time.
There are two places in our display routine where we need to compute the positions of folders: when we draw them and when we check for a mouse hovering over them. Thus, it is convenient to abstract out the positioning code into a Positioner class.
public class Positioner { private const int pLeft = 30; private const int pTop = 30; private const int HSpace = 70; private const int VSpace = 80; private const int rowMax = 2; private int x, y, cnt; //----- public Positioner() { reset(); } //----- public void reset() { x = pLeft; y = pTop; cnt = 0; } //----- public int nextX() { return x; } //----- public void incr() { cnt++; if (cnt > rowMax) { //reset to start new row cnt = 0; x = pLeft; y += VSpace; } else { x += HSpace; } } //----- public int nextY() { return y; } }
Then we can write a much simpler paint routine.
private void picPaint(object sender, PaintEventArgs e ) { Graphics g = e.Graphics; posn.reset (); for(int i = 0; i < names.Count; i++) { fol = folFact.getFolder(selectedName.Equals( (string)names[i])); fol.draw(g, posn.nextX() , posn.nextY (), (string)names[i]); posn.incr(); } }
The Class Diagram
The diagram in Figure 19-3 shows how these classes interact.
Figure 19-3. How Flyweights are generated
The FlyCanvas class is the main UI class, where the folders are arranged and drawn. It contains one instance of the FolderFactory and one instance of the Folder class. The FolderFactory class contains two instances of Folder: selected and unselected. One or the other of these is returned to the FlyCanvas by the FolderFactory.
Selecting a Folder
Since we have two folder instances, selected and unselected, we'd like to be able to select folders by moving the mouse over them. In the previous paint routine, we simply remember the name of the folder that was selected and ask the factory to return a "selected" folder for it. Since the folders are not individual instances, we can't listen for mouse motion within each folder instance. In fact, even if we did listen within a folder, we'd need a way to tell the other instances to deselect themselves.
Instead, we check for mouse motion at the Picturebox level, and if the mouse is found to be within a Rectangle, we make that corresponding name the selected name. We create a single instance of a Rectangle class where the testing can be done as to whether a folder contains the mouse at that instant. Note that we make this class part of the csPatterns namespace to make sure it does not collide with the Rectangle class in the System.Drawing namespace.
namespace csPatterns { public class Rectangle { private int x1, x2, y1, y2; private int w, h; public Rectangle() { } //----- public void init(int x, int y) { x1 = x; y1 = y; x2 = x1 + w; y2 = y1 + h; } //----- public void setSize(int w_, int h_) { w = w_; h = h_; } //----- public bool contains(int xp, int yp) { return (x1 <= xp) && (xp <= x2) && (y1 <= yp) && (yp <= y2); } } }
This allows us to just check each name when we redraw and create a selected folder instance where it is needed.
private void Pic_MouseMove(object sender, MouseEventArgs e) { string oldname = selectedName; //save old name bool found = false; posn.reset (); int i = 0; selectedName = ""; while (i < names.Count && ! found) { rect.init (posn.nextX() , posn.nextY ()); //see if a rectangle contains the mouse if (rect.contains(e.X, e.Y) ){ selectedName = (string)names[i]; found = true; } posn.incr (); i++; } //only refresh if mouse in new rectangle if( !oldname.Equals ( selectedName)) { Pic.Refresh(); } }