218 Third Test, 27 April 99

The Question

Bill Gates (All characters are fictional; any resemblance to real persons, living or dead, is purely coincidental.) has been arrested by the Feds and given a 5,000 year sentence in Alcatraz for crimes against humanity. You are part of the programming team responsible for re-implementing windows properly. The first stage is to define C++ classes that represent a computer's screen and the windows displayed on it.

You must define two classes: one called "window", that represents an individual window and records its size and position, and one called "screen" that represents the whole screen and records its size together with the collection of windows that are visible on it. One of your colleagues started the definition of "screen", but ran away before he finished. You may make use of his work:
class screen
{ protected:
    window *window_list[10];
    int num_windows;
    int width, height;
  public:
    screen(int w, int h);
    void add(window *x);
    void remove(window *x);
    window *find_window_at(int x, int y);
    void print(void); }

screen::screen(int w, int h)
{ width=w; height=h;
  for (int i=0; i<10; i+=1)
    window_list[i]=NULL;
  num_windows=0; }

void screen::add(window *x)
{ if (num_windows>=10)
  { fprintf(stderr,"Too many windows on screen\n");
    exit(1); }
  window_list[num_windows]=x;
  num_windows+=1;
  x->parent=this; }
The constructor's purpose should be obvious. The "add" method is used to add a window (that has already been created) to the screen. Every window has a member called "parent" which should always point to the screen that contains the window; that is the reason for the last assignment in the "add" method.

You must define the three remaining methods. "Remove" is the opposite of "add": give it a pointer to a window and it should remove that window from its list. "Find_window_at" is the method that is called when the user clicks a mouse button: give it x and y coordinates on the screen, and it searches through its list of windows to find the one that covers that position. "Print" is simply for debugging; it should print out the size of the screen, and a description of every window on that screen.

You will probably want to define the "window" class before defining these three methods. You should at least read the following description of the window class first.


The class "window" should have at least six members: integers "x" and "y" to represent the coordinates of the top left corner of the window; integers "width" and "height" to represent the size of the window; a string to store the name of the window, and a pointer-to-screen to represent the screen that this window appears on. You must define the class, and the following methods: Here is an example of the classes in use, so you can see how they should behave:
void main(void)
{ screen scr(640,480);                     // just a little old VGA monitor
  window *w1, *w2, *w3, *w4;
  w1=new window("One", 0, 0, 200, 100);    // creates a new window named "One";
  scr.add(w1);            // it is in the top left corner of the screen (x=0, y=0)
                          // and is 200 pixels wide and 100 pixels high.
  w2=new window("Two", 220, 0, 100, 100);  // a new window called "Two", just to
  scr.add(w2);            // the right of the first, and half the width.
  w1->moveto(0,100);      // the first window is moved to position (x=0, y=100)
  scr.print();         // Should make it print out something like this...
             // Screen size 640x480, contains 2 windows
             // Window "One" at 0,100, size=200x100
             // Window "Two" at 220,0, size=100x100
  w3=new window("Three", 400, 300, 200, 150);
  scr.add(w3);       // new window at (x=400, y=300) size 200 wide, 150 high;
  w4=new window("Four", 120, 200, 200, 200);
  scr.add(w4);       // new window at (x=120, y=200) size 200 wide, 200 high;
  w1->resize(0.75);  // first window changed to three quarters of its original size
           // see picture 1
  w3->swallow(w2);
           // w3 must cover the area originally covered by w3 and w2. w3 was at
           // (x=400, y=300) and reached to (x=600, y=450). w2 was at (x=220, y=0)
           // and reached to (x=320, y=100). To cover all of that, w3 must move to
           // (x=220, y=0) and reach to (x=600, y=450), so its size is 380x450.
           // see picture 2
  window *temp=scr.find_window_at(150,300);
  temp->print();       // window w4 ("Four") covers screen position (x=150, y=300)
                       // so this should print a description of window "Four".
  scr.print();    // Should make it print out something like this...
             // Screen size 640x480, contains 3 windows
             // Window "One" at 0,100, size=150x75
             // Window "Three" at 220,0, size=380x450
             // Window "Four" at 120,200, size=200x200
  scr.remove(w1);      // now w1 is removed, so only w3 and w4 remain.
  scr.print();    // Should make it print out something like this...
             // Screen size 640x480, contains 2 windows
             // Window "Three" at 220,0, size=600x450
             // Window "Four" at 120,200, size=200x200
(pictures omitted because HTML isn't that flexible)

The Answers

Defining the Window class. We have been told what members are to be provided, and what methods, so the class declaration should be easy:
class window
{ protected:
    int x, y, width, height;
    char *name;
    screen *parent;
  public:
    window(char *name0, int x0, int y0, int width0, int height0);
    void moveto(int newx, int newy);
    void resize(int new_width, int new_height);
    void resize(float proportion);
    void swallow(window *x);
    void print(void); };
The constructor hardly has to do anything; just initialise the members appropriately:
window::window(char *name0, int x0, int y0, int width0, int height0)
{ name=new char[strlen(name0)+1];
  strcpy(name,name0);
  x=x0;
  y=y0;
  width=width0;
  height=height0;
  parent=NULL; }
The next two methods are even easier; A window's position is stored in its x and y members and nowhere else. If you want to change its position, all you have to do is change its x and y, and that's it. The same goes for changing its size:
void window::moveto(int newx, int newy)
{ x=newx;
  y=newy; }

void window::resize(int new_width, int new_height)
{ width=new_width;
  height=new_height; }
We could have gone totally overboard and put in all sorts of error checking. For instance, we might decide that x, y, width, and height must never be negative and that the far edges (right and bottom) must never be off the edge of the parent screen. To do that, we could write:
int window::is_ok()
{ if (x<0 || y<0 || width<0 || height<0)
    return (0);
  if (x+width > parent->width ||
      y+height > parent->height)
    return (0);
  return (1); }
And then call that method after every change to see if everything is OK.
        The question is "why"? Why would you do all that extra work? You were not told to make any such checks, and the question as asked doesn't give you any reason to believe that there is anything wrong with windows that go over the edge of the screen. Most windowing systems allow that sort of thing. Don't invent extra work. Exams have quite enough questions already without you inventing new ones.

The version of resize that takes a floating point proportion is also simple, but you have to remember that you can't just assign a float value into an int variable; the compiler will whine at you. You have to convert the type explicitly:
void window::resize(float proportion)
{ width=(int)(width*proportion);
  height=(int)(height*proportion); }
All print has to do is print out the essential information. Nothing to it:
void window::print(void)
{ printf("  Window \"%s\" at %d,%d, size=%dx%d\n", name, x, y, width, height); }
All of those methods are really simple. Don't let that trick you into thinking you have missed something. A test will often have some very simple parts, just to test that you know how to do the really vital things, then some trickier parts to test your thinking a bit further. Swallow is a little bit trickier, but not much.

When one window swallows another, it takes on a size that covers the original positions of both itself and the swallowed window. To do that, we have to find the leftmost, topmost, rightmost, and bottommost edges of both windows, use those as the edges of the resized window. If we assume the existence of two functions "min" and "max", whcih do what their names suggest, it isn't too difficult. Just remember that although x and y do tell us the left and top edges of a window, the right and bottom edges have to be calculated as x+width and y+height.
(There is one tiny little complication. I'll avoid it for now, by changing the name of this method's parameter from x to w. I'll explain the complication soon):
void window::swallow(window *w)
{ int left=min(x, w->x);
  int top=min(y, w->y);
  int right=max(x+width, w->x+w->width);
  int bottom=max(y+height, w->y+w->height);
  x=left;
  y=top;
  width=right-x;
  height=bottom-y;
  parent->remove(x); }
That last statement is because the question said that the swallowed window should be removed from the screen. Screens have a remove method, and the current window's parent is obviously the screen in question.

The complication: if I had not changed the parameter's name to w, the expression for the leftmost edge would have been min(x, x->x) which is not only confusing, but also wrong. "x" can't mean two things at once. If we use x as the name of a method's parameter, we can not also use it to refer to a class member at the same time. The simplest solution is exactly what I did above: change the parameter's name. If you don't want to do that (although I can't imagine why you wouldn't), you can use "this" instead. The variable "this" is automatically declared inside every method, and is always a pointer to the current object, so every use of "x" to refer to the member x could be replaced by "this->x".

We can't just assume that useful functions exist. min and max must be defined, but that is trivial:
int min(int a, int b)
{ if (a<b) return a; else return b; }

int max(int a, int b)
{ if (a>b) return a; else return b; }



Now for the screen class. The question provided us with the beginning:
class screen
{ protected:
    window *window_list[10];
    int num_windows;
    int width, height;
  public:
    screen(int w, int h);
    void add(window *x);
    void remove(window *x);
    window *find_window_at(int x, int y);
    void print(void); }

screen::screen(int w, int h)
{ width=w; height=h;
  for (int i=0; i<10; i+=1)
    window_list[i]=NULL;
  num_windows=0; }

void screen::add(window *x)
{ if (num_windows>=10)
  { fprintf(stderr,"Too many windows on screen\n");
    exit(1); }
  window_list[num_windows]=x;
  num_windows+=1;
  x->parent=this; }
All we have to do is provide the definitions of remove, find_window_at, and print.


Remove has to be compatible with add. If we have five windows (A, B, C, D, E) and remove the third (C), then add another (F), we must be left with five windows (A, B, D, E, F) again. When remove gets rid of C, it must move D and E up to fill in the space. If we just reduce the variable "num_windows", the next add operation will just overwrite the last window.
        Remove should first look through the array "window_list" to find the unwantde window. It should then move all later windows up by one, and finally reduce "num_windows":
void screen::remove(window *w)
{ int i, pos=-1;
  for (i=0; i<num_windows; i+=1)
  { if (window_list[i]==w)
    { pos=i;
      break; } }
  if (pos==-1)  // no such window to remove
    return;
  for (i=pos; i<num_windows-1; i+=1)
    window_list[i]=window_list[i+1];
  num_windows-=1; }
Find_window_at must look through the list of existing windows, testing each to see if the given co-ordinates (x, y) are within its area. That is not too difficult like this:
window *screen::find_window_at(int x, int y)
{ int i;
  for (i=0; i<num_windows; i+=1)
  { window *w=window_list[i];
    if (x>=w.x && y>=w.y && x<=w.x+w.width && y<=w.y+w.height)
    { return (w); } }
  return (NULL); }
And that is a perfectly fine solution. It would be a lot neater if each window could simply be asked "do you contain this point?", so an alternative solution would be to add to the window class a "contains" method that returns 0 for no and 1 for yes:
int window::contains(int xpos, int ypos)
{ int right=x+width;
  int bottom=y+height;
  if (xpos>=x && xpos<=right && ypos>=y && ypos<=bottom)
    return (1);
  else
    return (0); }

window *screen::find_window_at(int x, int y)
{ int i;
  for (i=0; i<num_windows; i+=1)
  { window *w=window_list[i];
    if (w->contains(x,y))
    { return (w); } }
  return (NULL); }
This is slightly more readable, but certainly not required on the test.

Finally, we just have to design the print method for a screen; it should print out the screen's details, followed by the details of each window on the screen. Windows know how to print themselves, so we don't have to do much:
void screen::print(void)
{ int i;
  printf("Screen size %dx%d, contains %d windows\n", width, height, num_windows);
  for (i=0; i<num_windows; i+=1)
  { window_list[i]->print(); } }
And that's all it took.

The Extra Credit Bit

For this, you had to remove the limit on the number of windows that a screen can hold. It is not good enough to remove the "if (num_windows>=10)" from screen::add, because window_list is still just an array of 10 pointers. Instead we should use either a growable array or a linked list:

Using a growable array, we would change the beginning of the screen class definition to:
class screen
{ protected:
    window **window_list;
    int num_windows;
    int window_list_size;
    int width, height;
  ...
and the constructor becomes:
screen::screen(int w, int h)
{ width=w; height=h;
  window_list=new (window *)[10];
  for (int i=0; i<10; i+=1)
    window_list[i]=NULL;
  window_list_size=10;
  num_windows=0; }
and "add" is modified thus: void screen::add(window *x) { if (num_windows>=window_list_size) { window_list_size=num_windows+10; window **temp=new (window *)[window_list_size]; int i; for (i=0; i<num_windows; i+=1) temp[i]=window_list[i]; for (i=num_windows; i<window_list_size; i+=1) temp[i]=NULL; delete[] window_list; window_list=temp; } window_list[num_windows]=x; num_windows+=1; x->parent=this; }
If you want to use a linked list, we would have to define a special "list-of-window" class:
class list_of_windows
{ public:
    window *this_window;
    list_of_windows *next;
    list_of_windows(void) { this_window=NULL; next=NULL; } };
And screen would have to be modified again, changing the beginning of the class:
class screen
{ protected:
    list_of_windows *window_list;
    int width, height;
  ...
and the constructor becomes:
screen::screen(int w, int h)
{ width=w; height=h;
  window_list=NULL; }
Which is a lot smaller, and "add" is modified thus: void screen::add(window *x) { list_of_windows *temp=new list_of_windows(); temp->this_window=x; temp->next=window_list; window_list=temp; x->parent=this; } Which is also nice and easy. Sadly, we would also have to modify remove. It would have to search through the list of windows until it finds the unwanted one, then modify the "next" pointer of the previous link to skip over the found one:
void screen::remove(window *w)
{ list_of_windows *ptr=window_list;
  list_of_windows *prev=NULL;
  while (ptr!=NULL &&ptr->this_window!=w)
  { prev=ptr;
    ptr=ptr->next; }
  if (ptr==NULL)     // no such window found
    return;
  if (prev==NULL)    // removeing very first window
    window_list=ptr->next;
  else
  { prev->next=ptr->next;
    delete ptr; } }
Which is perhaps not the simplest thing in the world, but we've all seen much worse. As far as the test was concerned, linked lists and growable arrays are equally acceptable. For your pruposes, a growable array would be best because it involves less work. The two other screen methods, find_window_at and print would also need to be modified to work with a linked list of windows, and that would add up to quite a lot of extra work.