Skip to main content

Antonio Cisternino's Home Page

Go Search
Home
  
Antonio Cisternino's Home Page > My Blog > Generic Layout Managers  

My Blog: Generic Layout Managers

Title

Generic Layout Managers 

Body

During my intern in MSR Cambridge in 2001 I was lucky and able to test .NET Generics on their first implementation (the one afterward published as Gyro, an extension to SSCLI, and available at http://www.sscli.net/).
Although I was already working with .NET, my background was on Java and at the time I was missing very much layout managers in Windows Forms. Playing with generics I realized that generic types allows defining trees, and also windows (in the wider meaning all components are based on a windows) are organized in a tree structure.
I decided to define generic layout managers.
Let's begin with a first example just to make clearer the idea:
 
 public class HPair<T,U> : Control where T : Control, new() where U: Control, new() {
  public T First;
  public U Second;
  public int Amount;
  public HPair() : this(-1) {}
  public HPair(int p) {
   First = new T();
   Second = new U();
   Amount = p;
   Controls.Add(First);
   Controls.Add(Second);
   SetSize();
   Resize += delegate { SetSize(); };
  }
  private void SetSize() {
   if (Amount == -1) return;
   First.Width = (int)(this.Width * (Amount / 100.0));
   Second.Width = (int)(this.Width * ((100 - Amount) / 100.0));
   Second.Left = First.Width;
   First.Height = Second.Height = Height;
  }
 }
The class HPair is able to layout two graphics controls of the given type. We see that we use bounds on type arguments to be able to assume that these types inherits from Control and have the parameterless constructor.
Once we have this class we can build a labeled text box:
 
HPair<Label, TextBox> lb = new HPair<Label, TextBox>(10);
 
The argument specified to the constructor indicates the proportion of the left component with respect to the whole container (in this case 10% of the with is used for the lavel).
In a classical solution in Java or .NET 1.x we could have defined a generic class HPair with two instance variables of type Control and rely on the subtype polymorphism in order to accomplish the task. So, what's the point in using a generic type? It is easy to see that it is neither a matter of performance. The reason is: we avoid casts because we inform the compiler about the nature of objects contained into the layout manager. For instance we can access members without bothering about casts being sure of types. Thus we can set the label text as follows:
 
lb.First.Text = "MyLabel";
 
In this case the compiler knows that First is of type Label without having to wait runtime to discover it!
With few changes I was able to define also the vertical pair of components:
 
 public class VPair<T,U>: Control
  where T: Control, new()
  where U: Control, new() {
  public T First;
  public U Second;
  public int Amount;
  public VPair() : this(-1) {}
  public VPair(int p) {
   First = new T();
   Second = new U();
   Amount = p;
   Controls.Add(First);
   Controls.Add(Second);
   SetSize();
   Resize += delegate(Object sender, EventArgs e) {
    SetSize();
   };
  }
  private void SetSize() {
   if (Amount == -1) return;
   First.Height = (int)(this.Height * (Amount / 100.0));
   Second.Height = (int)(this.Height * ((100 - Amount) / 100.0));
   Second.Top = First.Bottom;
   First.Width = Second.Width = Width;
  }
 }
 
It was rather obvious to extend the idea to a row of components:
 
 public class HRow<T>: Control
  where T: Control, new() {
  private int N;
  public int Length {
   get { return N; }
   set {
    N = value;
    for (int i = 0; i < N; i++)
     Controls.Add(new T());
    SetSize();
   }
  }
  public HRow() {
   N = 0;
   Resize += delegate(Object sender, EventArgs e) {
    SetSize();
   };
  }
  public HRow(int n) : this() {
   this.Length = n;
  }
  private void SetSize() {
   int s = Width / N;
   int pos = 0;
   foreach (Control c in Controls) {
    c.Left = pos;
    pos += s;
    c.Width = s;
    c.Height = Height;
   }
  }
  public T this[int i] {
   get { return (T)Controls[i]; }
  }
 }
 
In this case the horizontal space available to the container is distributed evenly among the components in it. The container is homogeneous, and we defined an indexer so that refer any of the contained controls is as easy as to access an array.
As in the case of HPair it was natural to define the column of controls:
 
 public class VRow<T>: Control
 where T: Control, new() {
  private int N;
  public int Length {
   get { return N; }
   set {
    N = value;
    for (int i = 0; i < N; i++)
     Controls.Add(new T());
    SetSize();
   }
  }
  public VRow() {
   N = 0;
   Resize += delegate(Object sender, EventArgs e) {
    SetSize();
   };
  }
  public VRow(int n) : this() {
   this.Length = n;
  }
  private void SetSize() {
   if (N == 0) return;
   int s = Height / N;
   int pos = 0;
   foreach (Control c in Controls) {
    c.Top = pos;
    pos += s;
    c.Height = s;
    c.Width = Width;
   }
  }
  public T this[int i] {
   get { return (T)Controls[i]; }
  }
 }
 
I realized then that inheritance would have allowed me to combie containers into complex ones: a column of rows is a table! So, almost effortlessy I defined the table:
 
 public class Table<T> : VRow<HRow<T>>
  where T: Control, new() {
  public Table() : base() {
  }
  public Table(int r, int c) : base(r) {
   for (int i = 0; i < r; i++)
    this[i].Length = c;
  }
  public T this[int i, int j] {
   get { return (T)((HRow<T>)Controls[i]).Controls[j]; }
  }
  public int Rows {
   get { return this.Length; }
   set { this.Length = value; }
  }
  public int Columns {
   get { return this.Length == 0 ? 0 : this[0].Length; }
   set {
    if (this.Length == 0) throw new ArgumentException("No rows!");
    for (int i = 0; i < this.Length; i++)
     this[i].Length = value;
   }
  }
 }
 
As you see I simply added the indexer to have an array-like access to the controls into the container.
We were pleased by the table definition: it is a rare example of (meaningful) complex inheritance from a generic type.
To test the framework I decided to develop a really complex application: a full fledged calculator. The following method prepares a form with all the logic needed:
 
  static Form MakeCalculator() {
   Form f = new Form();
   f.Size = new Size(300, 200);
   f.Visible = true;
   VPair<Label,VPair<HPair<Table<Button>,VRow<Button>>,HPair<Button,Button>>> hp = new VPair<Label,VPair<HPair<Table<Button>,VRow<Button>>,HPair<Button,Button>>>();
   Label display = hp.First;
   int currValue = 0, accumulator = 0;
   char oper = ' ';
   hp.First.Text = "0";
   hp.Amount = 5;
   hp.Second.Amount = 75;
   hp.Second.First.Amount = 75;
   hp.Second.First.First.Rows = 3;
   hp.Second.First.First.Columns = 3;
   EventHandler cl = delegate (Object tgt, EventArgs e) {
      char c = (tgt as Button).Text[0];
      if (c >= '0' && c <= '9') {
      currValue = currValue * 10 + c - '0';
      display.Text = currValue.ToString();
      return;
     }
     if (c == 'C') {
      currValue = accumulator = 0;
      display.Text = "0";
      return;
     }
      switch (oper) {
     case '+':
      accumulator += currValue;
      break;
     case '-':
      accumulator -= currValue;
      break;
     case '*':
      accumulator *= currValue;
      break;
     case '/':
      accumulator /= currValue;
      break;
     case ' ':
      accumulator = currValue;
      break;
     }
     display.Text = oper == ' ' ? currValue.ToString() : accumulator.ToString();
      if (c == '=') {
      currValue = accumulator = 0;
      oper = ' ';
     } else {
      currValue = 0;
      oper = c;
     }
   };
   for (int i = 0; i < 9; i++) {
    hp.Second.First.First[i / 3, i % 3].Text = (i + 1).ToString();
    hp.Second.First.First[i / 3, i % 3].Click += cl;
   }
   hp.Second.First.Second.Length = 5;
   hp.Second.First.Second[0].Text = "+";
   hp.Second.First.Second[1].Text = "-";
   hp.Second.First.Second[2].Text = "*";
   hp.Second.First.Second[3].Text = "/";
   hp.Second.First.Second[4].Text = "=";
   for (int i = 0; i < 5; i++)
    hp.Second.First.Second[i].Click += cl;
   hp.Second.Second.Amount = 75;
   hp.Second.Second.First.Text = "0";
   hp.Second.Second.First.Click += cl;
   hp.Second.Second.Second.Text = "C";
   hp.Second.Second.Second.Click += cl;
   f.Controls.Add(hp);
   hp.Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Top | AnchorStyles.Right;
   hp.Bounds = new Rectangle(0, 0, f.Width, f.Height);
   return f;
  }
The layout of the controls forming the calculator is defined by the long (long, long, ...) type of the variable hp. We first setup the various components by simply accessing their members.
We then define an anonymous method (new feature of C# 2.0) which defines the logic of the calculator. The method is designed to be an event handler for all the buttons, so we rely on the button label to decide what action to take.
An interesting thing to note is that the state of the calculator is defined by three local variables of the MakeCalculator method: currValue, accumulator and oper.
We close these variables into the anonymous method, and the compiler takes care that their values will be available as long as the anonymous method is alive (after all this is the good of closures).
After defining the event handler we set all the properties required to define the layout of the calculator.
The calculator can be displayed using the following Main method:
 
  public static void Main(string[] args) {
   Application.Run(MakeCalculator());
  }
An interesting fact to notice is that this library, as it is, can't be rewritten for Java with generics! We rely on the ability of creating instances of type arguments into generic types: this cannot be done in Java because generics aren't implemented in the Java Virtual Machine.
Ehm... I forgot to tell you something... I still have some problem in the proper layout of components: the calculator will work fine, but you'll notice that buttons tend to overlap a little bit. If somebody finds the bug I will be grateful :)

Expires

 

Category

Programming 
Attachments
Created at 10/2/2004 22:02  by Antonio Cisternino 
Last modified at 10/2/2004 22:57  by Antonio Cisternino