The Prism Primer - Part 8

From The Oxygene Language Wiki

Jump to:navigation, search

This is a General topic
Feel free to add your notes to this topic below.


The Prism Primer: Part 1Part 2Part 3Part 4Part 5Part 6Part 7Part 8Part 9

Object-oriented Programming

(Please read Preliminary Work: A Glimpse of Classes again.)

For this chapter we'll add a new project to the solution, again a Windows Forms Application. Call it "OOPSample" or whatever else you want.

Instead of placing controls onto the form, we'll add a new file to the project to put our first class in it. In order to do that, click with the right mouse button on the project in the solution explorer, choose "Add" and then "New item".

Delphi Prism offers different item templates for different things you may want to add to a project. The template for a new class should be already selected. You have to specify the name for the new file, which will be the initial name for the class, too. Choose "MyRectangle.pas" as filename. "pas" is the extension for all Delphi Prism source files, because it's as pascal language.

There should be the stub of a class in the file, which looks like this:

type
  MyRectangle = public class
  private
  protected
  public
  end;

The class MyRectangle is supposed to describe a rectangle. That's basically what OOP is all about: describing objects with their properties and things they can do. Of course, you need a width and a height to describe a rectangle. One could also think of calculating a rectangle's area. Let's add that to our class:

MyRectangle = public class
private
protected
public
  property Width : Single;
  property Height : Single;

  method GetArea : Single;
end;

Hitting Ctrl+Shift+C will create the GetArea method. Now we'll write the method body.

Self

GetArea should of course return the product of the rectangle's width and height. So this is the method body:

result := Width * Height;

Looks simple - but isn't. Width and Height are not only variables, they're properties in the MyRectangle class. Now you can have an arbitrary number of rectangles in your project (these "concrete" rectangles are called instances of the MyRectangle class). The GetArea method has to take the right width and height to calculate the area of one of these rectangles. So Width and Height in the method body are the width and height of the rectangle the GetArea method is called for.

It may be that you want to directly refer to the instance a method belongs to. That's what the keyword self is for. For example, tbInput referred to a specific textbox, self refers to the current instance. You can write the method body for GetArea like this:

result := self.Width * self.Height;

In fact the first version is just a shortcut version of the second one, it will compile exactly the same.

Using a Class / Creating an Instance

Let's get back to our program. The goal is to enable the program to draw different kinds of geometric objects. For now it's enough that it is able to calculate the area of a rectangle.

Please switch back to the form designer by clicking on "Main.pas [Design]" in top of the editor. Add the following components to the form:

Switch to the button's click handler by double clicking on it.

You already know how to use simple types like Integers, Doubles or Strings (although the latter are a bit more complex than one can see at first). Using classes is a little bit more complex: You have to create a new instance of a class in order to use it.

A variable (or parameter) of a class type, which hasn't been "created", has the value nil (a keyword), which stands for "not in list". In other word, it stands for "nothing". You could also say that it doesn't have a value.

That's very different from types like for example Integers, which always have a value. These value types cannot be nil. But classes are reference types and therefore they don't need to have a value. And as reference types they show the same behaviour as arrays regarding their usage as parameters.

For creating new instances, every class has a constructor. If you don't define you own (which we'll do later), a default constructor is used. The constructor is called using the new keyword followed by the class name:

var myRect := new MyRectangle;

Now we can access the rectangle's properties like we accessed the properties of our textboxes:

myRect.Width := Single.Parse(tbWidth.Text);
myRect.Height := Single.Parse(tbHeight.Text);

Now we can display the area of this rectangle:

MessageBox.Show(myRect.GetArea.ToString);

Here's a small demonstration of class types that are reference types:

var anotherRect := myRect;
anotherRect.Width := 42;
MessageBox.Show(myRect.GetArea.ToString);

This will show a changed area for myRect, too. Because MyRectangle is a reference type, there only was a new name introduced for the same rectangle. Both variables, myRect and anotherRect are references to the same instance, to the same region of the computer's memory. This memory is not copied when assigning one reference type variable to another.

Own Classes vs. Built-in Classes

Perhaps it's good to make this clear: There is no difference between "your" classes and the classes in the framework. A textbox you place on the form is an instance of the TextBox class. When adding event handlers and other methods to a form, you're changing the MainForm class. The form you see on your monitor is an instance of this MainForm class.

The only difference is that the MainForm class and the textboxes and buttons can be changed by a visual form designer in order to achieve a WYSIWYG environment. But that's nothing special - you can write classes that make use of the form designer, too.

Always keep in mind: your classes and the built-in classes work exactly the same. Everything a framework's class can do can also be done in an "own" class.

Properties and Fields

When running the program, you could have the idea to enter a negative number for either the width or the height. After clicking on the "Area" button, a negative area will be displayed. That does not make much sense.

We will now extend the class in order to prevent it form accepting negative widths and heights. For that we add a getter and a setter to our property. First look at the syntax (but don't change your project yet, there'll be a nice way to do that):

private
    fWidth: Single;
    method set_Width(value: Single);
  public
    property Width : Single read fWidth write set_Width;
Fields

We have introduced a few new things here. There's the line fWidth : Single: this is called a field. You can think of a field as a variable which isn't valid only for a method but for a whole instance of class. You should try to only use fields for internal things in your class, for other things you should use properties.

There are different ways to name fields. One is to prepend them with an "f", which I'll do here. You should choose one naming-method to distinguish fields from properties.

Getter and Setter
Adding a field as getter
Adding a method as setter

The set_Width method is just a normal method taking one parameter. Both the field and the method are used in the line where the property is defined. The property's defintion was extended by two parts:

  1. The defintion of a getter: read fWidth
  2. The definiton of a setter: write set_Width

Now, when the value of the property is read (e.g. by myRect.Width.ToString), actually the value of fWidth is read and returned as width. And when the value is written (e.g. myRect.Width := -5), the method set_Width is called with the value parameter set to 5.

The task is to complete the set_Width method, so it somehow stores the assigned value (and of course it should do that in fWidth so it can be easily read again).

By the way: It's not much additional work to define a property with a getter and a setter. Delphi Prism will create the field and the method for you. Just start typing "read fWi.." and you'll see an entry for "fWidth" in the code completion, marked with a green plus sign (maybe you won't - then you can press "Ctrl+J" to make the menu appear). That means that this field will be created when you hit enter. The same happens when you type "write set_": a method "set_Width" will be created via code completion. An empty method body is created as well.

Please extend the width property in your project now and let Prism help you with that. Then go to the method body of the setter method. To prevent negative values the body could look like this:

method MyRectangle.set_Width(value: Single);
begin
  if value > 0 then
    fWidth := value;
end;

Only if a non-negative value is assigned to the width-property, the value gets actually stored. All other assignments are discarded. That's not the best way of handling wrong values, raising some kind exception would be much better, but this way is enough for now.

To clarify what happens when someone accesses the width-property now:

Please extend the height-property with a field as getter and a method as setter, too.

A property does not need to have a setter, it can be a read-only property with just a getter. We can implement a property "Area" in this way. We only have to add a new property to thehe class definition, our methods stay the same:

property Area : Single read GetArea;

We've introduced the Area property using GetArea as getter method. As you can see, too, the name of the method isn't important, it just has to have the correct return type (Single in this case). Now, everytime someone uses the Area property, the area is calculated and then returned as this property.

Encapsulation and Visibility

An important idea in OOP is encapsulation. The most common way to achieve encapsulation is using differnt visibilities. This means, that you can make things visible to the "public" (e.g. for you MainForm) and other things are "private" and only visible inside your class. Look at the definition of the MyRectangle class:

MyRectangle = public class
private
  fHeight: Single;
  fWidth: Single;
  method set_Height(value: Single);
  method set_Width(value: Single);    
public
  property Width : Single read fWidth write set_Width;
  property Height : Single read fHeight write set_Height;
  property Area : Single read GetArea;

  method GetArea : Single; //WRONG at this place, as we'll see in a few paragraphs!
end;

There's a part with a prepended private keyword and one part with a prepended public keyword. All the private declarations are only visible inside your class, e.g. in the GetArea method. You could access the private field fWidth from within that method, but you couldn't access it from one of the MainForm's methods. The MainForm (and almost any other class) only "sees" the public declarations!

Important: The information, what a propertie's getter and setter are, is always private even if the property itself is public. Publically visible is only, if the property is readable, writabe or both!


Encapsulation enables the progammer of a class to hide its implementation details without breaking code which is using the class. Let's say we decide, that we don't want to store width and height anymore, but width and area. We would've to introduce a new field fArea and the getter of the Height property now would be a method calculating the height rather than reading it from the field.

A code using only the public properties would still be fully functional! Everything "under the hood" changed, but that doesn't matter, because it is encapsulated! And because of that, it is very important to only make as few things as possible publicly available.

Everything you make public is very hard to change afterwards, because there could be code using it. Look at the last version of the MyRectangle class: The GetArea method is public and therefore could be used by another class (in fact we use it in our MainForm class). We cannot simply replace it by the fArea field, because that could break existing code.

And that's the reason why you should keep the getters and setters of your properties, regardless of wether they are fields or methods, private! The place, where the GetArea method is declared at the moment, is wrong! It should be in the private section, where only our class can use it. Every other class is supposed to access the Area property.

Please move the GetArea method to the private section and adjust the code in the MainForm to only access the Area property.

Summary

That could've been a little bit too much at once. So, here's the summary:

Type Visibility and Namespaces

You may have noticed, that the class itself has the visibility modifier public on itself, too. That is because in .NET (and thus Delphi Prism) types have a visibility. Suppose you want to write a library for working with geometrical objects. Then you'll have classes that are supposed to be used from other projects that utilize your library. You make these classes public.

But you'll also have classes, that are for library-internal use only and other projects using your library shouldn't see those classes. Then you mark them with the visbility modifier assembly (or just omit the public keyword, because then they've assembly-visiblity by default).

An assembly is (simply speaking) what you get when you compile your project. Assemblies can be single exe- or dll-files. An element with the assembly-visiblity will therefore be visible to every other element in the same assembly, but not to elements in other assemblies.

Namespaces

(We leave the topic "OOP" a little bit, but this fits pretty well in here ;-))

Perhaps this is a good point to talk about namespaces, too. All classes in the .NET Framework are logically structured in namespaces. Namespaces are completely independent from assemblies. An assembly can add classes to different namespaces and a namespace can contain classes from different namespaces.

For example can components like Button or Textbox be found in the namespace "System.Windows.Forms". But by changing the first line in one of our files from "namespace OOPSample" to "namespace System.Windows.Forms", we extend this namespace with our class. A program using our assembly will find our class in this namespace. The namespace then contains classes from at least two assemblies.

Or we could decide to make a logical partition between our GUI (the form) and our classes. The form would stay in the "OOPSample" namespace, by changing the first line in the classes' files we can put them in a new sub-namespace "OOPSample.Classes". We would then have two namespace in one assembly!

Namespaces are a really powerfull method to keep your classes in order and to quickly find the class you need. And they make it possible, to have classes that have the same name, as long as they are in different namespaces. You can write your own Button-class, you just can't put it in the System.Windows.Forms-namespace ;-)

That's why you have to tell the compiler which namespaces (not files, like it was in the "old" delphi days!) you want use in a file. You do that in the uses-section right at the beginning of the interface-section. In most cases the most important namespaces are already in there and the form designer adds the approriate namespaces for buttons, etc automatically, too.

Types not in uses, but in CC

By default the code completion will show only types from namespaces, that are in you uses-clause. But Delphi Prism has a very cool feature: It can display types from namespaces not in your uses-clause and when you use them, it automatically adds the namespace to your uses clause. You can activate this behaviour in the options: Tools -> Options -> Text Editor -> Delphi Prism -> Code Completion. Check "Show types not in current 'uses'" and select "Add namespace to interface section uses clause".

The first option "Add namespace to type declaration" would lead to the behaviour, that the namespace isn't added to the uses-clause but prepended to the type name. That is a possibility to use a type not in your uses-clause or to make clear which type you want to use, if there're types with same names in different namesapces. So, you can write System.Windows.Forms.Button or Cool.Components.Button to access both classes, regardless if one of the namespaces is in the uses-clause.

By the way: You should active "Automatically drop CC list when typing", too. It spares you hitting Ctrl+Space to get code completion :-)

And now: Back to OOP!


Inheritance

We want to add another geometric objects to our program, e.g. a circle. Please add a new class to your program as you've done it with the MyRectangle class. Call it "MyCircle" and name the file accordingly. The class for a circle could look like this:

MyCircle = public class
private
  fRadius: Single;
  method GetArea: Single;
  method set_Radius(value: Single);
public
  property Radius : Single read fRadius write set_Radius;
  property Area : Single read GetArea;
end;

The impplementation of the GetArea method is of course:

result := fRadius * fRadius * Math.PI;
The Idea

Now we see, that both MyRectangle and MyCircle have a property for their area. You can call it a common property of all geometric objects that they have an area, regardless of the way on how to calculate it. You have the abstract idea of an geometric object and its properties. And then you have concrete geometric objects that implement these properties.

Or nearer to real-life: You have the abstract idea of "furniture" with properties "color" or "material", and you sit on your chair at your desk both implementing these properties.

This is something the concpet of inheritance tries to represent. You introduce more abstract classes that other classes are derived from ("Chair" would derive from "Furniture"). We want to introduce a "MyGeomObject" class with a property "Area" both MyRectangle and MyCircle inherit.

The Basic Class (aka Abstract Classes)

Please add a new class to your project, call it "MyGeomObject". This class should look like this:

type
  MyGeomObject = public class //This line isn't complete, yet. See below for explaination.
  public
    property Area : Single read GetArea;
  protected
    method GetArea: Single; abstract;
  end;

You will have to (a) change the private visibilty to protected, (b) add the abstract; to the method and (c) remove the method body, because in this class we don't "know" yet how to implement that mehtod.

Members of a class that are protected are like private methods for alien classes, but can be "seen" by derived classes. So when we derive MyRectangle and MyCircle from MyGeomObject, these two classes will be able to see the GetArea method, but outside classes won't be able to do that - encapsulation is preserved.

We marked the GetArea method with the abstract keyword, because we don't want to provide an implementation at this point, but only say "Hey, this method exists!".

When you try compile this definition, you'll succeed but get a warning:

(PW13) non-abstract class "OOPSample.MyGeomObject" does not provide implementation for abstract method "GetArea: System.Single"

Classes with abstract members must be abstract, too. To get rid of this warning, just add the keyword abstract between "public" and "class" in the second line.

Note: You cannot instantiate abstract classes. They only provide a "prototype" for descending classes. A descending class will have to provide implementations for all abstract members, otherwise it must be marked as abstract, too!

Deriving One Class From Another

Now we have to derive the classes MyCircle and MyRectangle from the base class MyGeomObject. They will have to actually implement the GetArea method and inherit the Area property which uses this method.

Let's start with the MyRectangle class. To derive this class from MyGeomObject, we have to change its declaration:

type
  MyRectangle = public class(MyGeomObject)
"Smart Tag" to Implement abstract members

You will see a small blue box under the word "MyGeomObject". It's called a "smart tag". Clicking on it will show you an menu, which offers you a funtion to automatically implement the abstract methods of the abstract class (the GetArea method in this case). We won't use this feature at this point (there is already such a method in this class), but in most cases it is very usefull.

Now we just have to move the existing GetArea method from the private section to a protected section, which has to be created, too. Remove the Area property from this class, too - it will inherit the property from the MyGeomObject class.

There's just detail to do - but an important detail. We have to tell the compiler, that our GetArea method is the implemenation of the abstract method of MyGeomObject and overrides this abstract method. We do that be adding the override keyword to the method declaration:

method GetArea : Single; override;
.

Your class declartion now should look like this:

MyRectangle = public class(MyGeomObject)
private
  fHeight: Single;
  fWidth: Single;
  method set_Height(value: Single);
  method set_Width(value: Single);  
protected
  method GetArea : Single; override;
public
  property Width : Single read fWidth write set_Width;
  property Height : Single read fHeight write set_Height;
end;

We don't have to change the implementation of the class, because we still have the same methods as before.

You should be able to change the MyCircle class accordingly.

First Benefits: Implement One, Get Two

Objects can have common properties. For example both a chair and a bed have a position in room. Every furniture has a position in room. The same is true for our geometrical objects. So, we don't need to implement this property for each class, but we can do it in our base class MyGeomObject.

Add these two public properties to the MyGeomObject class:

property X : Single;
property Y : Single;

These are actually implemented properties which need not to be implemented in a derived class. An abstract class can have implemented members!

And the best thing about it: Because both MyRectangle and MyCircle inherited from MyGeomObject, they both now have the properties X and Y. You can even add getters and setters to the properties in the base class in order to throw an exception if someone tries to set a negative value - both derived classes will use these setters and getters.

Squeezing Circles In Our GUI (and "Extended Constructor Calls")

Perhaps you have already noticed it, we haven't yet updated our form to support the MyCircle class. We want to somehow squeeze this into our existing GUI, for several reasons. First, for showing the correct way I would've to explain some other things and that would get us too far away from the topic at hand ;-) Second, I can show you another application of inheritance.

RadioButton element in the toolbox

First, we'll rename the "Width" textbox and label into "Width or Radius". So, change the names to "tbWidthOrRadius" / "lbWidthOrRadius" and the label's text to "Width or Radius".

Now add two RadioButtons to the form. With RadioButtons the user can select one of several options you give him. Call the RadioButtons "rbRectangle" and "rbCircle", set their Text-properties to "Rectangle" and "Circle". Additionally you should define the RadioButton which is initially selected when the program starts: Set the Checked-property of the first RadioButton to true.

Now, switch to the click handler of the existing "Area"-button. This time, I'll give you the code and then explain, what it does:

var geomObject : MyGeomObject;

if rbRectangle.Checked then
  geomObject := new MyRectangle(
    Height := Single.Parse(tbHeight.Text),
    Width := Single.Parse(tbWidthOrRadius.Text)
  );

if rbCircle.Checked then
  geomObject := new MyCircle(
    Radius := Single.Parse(tbWidthOrRadius.Text)
  );

MessageBox.Show(geomObject.Area.ToString);

Okay, let's start with the last line. There we use the Area property of some geometrical object and display its value. We don't know (and care), if this object is a rectangle or a circle. It's a geometrical object, it has an Area property, we can display it. Finished.

Now we jump to the first line. Remember, that I said with type inference you'd almost never have to actually declare a variable with its type? Well, this is one of the rare cases where you have to ;-). We declare the geomObject as a variable of type MyGeomObject. We can't use type inference here, because we don't want to declare the variable as one of the derived types (MyRectangle and MyCircle), but as the base type MyGeomObject, regardless of what type of object we put in later.

The following if-clause for itself should be clear, but the syntax of the constructor call is new. This is called an extended constructor call. It combines the instanciation of an object with the assignment of values to some of its properties. This, and the reason why I used the new syntax, will get clearer if we look at the equivalent for this call:

geomObject := new MyRectangle;
geomObject.Height := Single.Parse(tbHeight.Text);
geomObject.Width := Single.Parse(tbWidthOrRadius.Text);

Now it should be clear, what the extended constructor call does. If you'd try to compile this sourcecode, you would get a compiler error, that says, that geomObject does not have a Height or Width property. We have declared geomObject as MyGeomObject, so the compiler "knows" (without help) only about the properties this class has. And that is only the read-only property Area.

When using the extended constructor call the compiler "knows" that it is dealing with a MyRectangle, because we create it and assign the properties in one go. Another way to avoid this problem would be casts: with casts we tell the compiler that a variable of one type is actually of another type. Like that:

MyRectangle(geomObject).Height := Single.Parse(tbHeight.Text);

But you have to admit, that the extended constructor call is more elegant ;-)

Back to the original sourcecode: the second if-clause does the same thing for circles. Remember that only one RadioButton can be checked, so only one if-condition will be true. After the two if-clauses, myGeomObject is either an instance of MyRectangle or MyCircle. But in the last line that doesn't matter any more: there's a geometrical object with an area, we display it. In the last line, we can completely forget about where this object has come from!

You can now run your program and calculate the area of rectangles and circles with just one method.

Intermezzo: Make The GUI Ready For More

There's still one topic of OOP I want to talk about, but for that and one ohter pending topic, we have to do some major changes to the GUI. To get those changes out of the way without disturbing the "real" topics, we'll do them in this seperate section.

The goal is, that the user will be able to add different geometrical objects to a list and the objects in this list then can be drawn to the screen. So, what we need is (a) such a list, (b) textboxes for the positions (X and Y) and (c) an area to draw on. And additionally we'll get rid of the RadioButtons and replace them with something more appropriate. :-)

First remove the Radiobuttons. We won't need them anymore. Select them and hit the Delete-Key. Then you can re-rename the tbWidthOrRadius textbox (and label) back to "Width". Now, make the form big, we want to place a lot of components on it. Then add a TabControl on it, name it "tcGeometricalObjects". A TabControl can have many tabs and different controls can be placed on each of them. Select the textboxes and labels (not the Area-Button) and drag them into the first tab.

Two tab pages of a tab control

To get the properties for a tab, you have to click into its area (inside the dashed line), not on its header. Change the name of the first tab page to "tpRectangle" and change the text property accordingly. Switch to the second tab (by clicking on its header) and name it "tpCircle" (and change its text property).

Place a textbox and a label for the radius on it, give them both the correct names (and the correct text for the label). You should now have the tab control with two tab pages, one for a rectangle and one for a circle. Each tab page should have the textbox(es) to specify the dimensions of the according geometrical object.

Switch to the code for calculating the area. As the radiobuttons are gone, we now have to change it. Replace the rbRectangle.Checked by tcGeometricalObjects.SelectedTab = tpRectangle. The SelectedTab property of the tab control contains the tab that is currently in the foreground. You can set it to set the displayed page programmatically or read it, to determine which tab is displayed currently.

Replace the if-condition for the circle accordingly. Additionally, you'll have to change the textboxes the values are read from. Both width of the rectangle and radius of the circle have their own textboxes, now.

Next thing on our todo list are textboxes for the coordinates. Add two textboxes (and two labels) for X and Y (with the usual names and texts) outside of the tab control below it. Because every geometrical object has these properties, it does not make sense to place them on one of the tab pages. We also need a new button called btAdd (with an appropriate text property), add it below the textboxes.

GUI of the sample application

As a component we didn't use yet, we add a ListBox to the form below the add-button. A ListBox displays a list of seperate objects, that can each be selected by the user. To be more precise: it displays the string representation of those objects, but it holds the whole objects. Name the listbox "lbGeomtricalObjects". Below it add a button called btDraw.

Last component we need is a PictureBox. Make it big and place it right of all other components. A picture box can display a great variety of pictures. That can be files on your harddrive (you would use the ImageLocation property for that) or a bitmap in the computer's memory, which is what we will use to display our geometrical objects. Give the PictureBox the name "pbCanvas".

The "GUI upgrade" is finished now. Don't be worried, we won't use all elements at once, but enhance the program step by step.

Adding objects (aka "Virtual Methods")

Please create the click method for Add-button by double clicking on it. When this button is clicked, the program should add a geometrical object to the items of the listbox. In order to do so, it has to create the object the same way we did in the Area button click, additionally assign the coordinates and then add this instance to the list.

Creating the item is completely the same in both click methods, so we'll create a method for that and use that in both click handlers:

method MainForm.CreateGeomObject : MyGeomObject;
begin
  if tcGeometricalObjects.SelectedTab = tpRectangle then
    result := new MyRectangle(
      Height := Single.Parse(tbHeight.Text),
      Width := Single.Parse(tbWidth.Text)
    );

  if tcGeometricalObjects.SelectedTab = tpCircle then
    result := new MyCircle(
      Radius := Single.Parse(tbRadius.Text)
    );
end;

method MainForm.btArea_Click(sender: System.Object; e: System.EventArgs);
begin
  var geomObject := CreateGeomObject;
  MessageBox.Show(geomObject.Area.ToString);
end;

This shows the new method and its usage in the existing click handler for the Area-button (don't forget to hit Ctrl+Shift+C after pasting in the new method). We can again use type inference for the geomObject, because the return type of the CreateGeomObject-method is MyGeomObject, the abstract base class.

Notice, that instances of derived classes can be the result of a method that has the base class as result type!

The Add-button's click handler will create the geomObject by using the new method and then assign the X and Y property:

geomObject.X := Single.Parse(tbX.Text);
geomObject.Y := Single.Parse(tbY.Text);

No casts are necessary here, because these properties are declared in the MyGeomObject base class. Then add this geometrical obejct to the listbox's items:

lbGeomtricalObjects.Items.Add(geomObject);

The Add method takes an object of class Object as parameter. As every class is derived from Object, you can add variables of every type to a listbox's items list.

You can now run the program and add some geometrical objects to the listbox. A littel bit disappointing is, that they're only displayed in the form

OOPSample.MyRectangle
OOPSample.MyCircle

To change this behaviour, we have to implement the ToString method. This method is a virtual method declared in the Object class, which is the base class of all classes in the .NET framework and therefore in Delphi Prism. A virtual method is like an abstract method, with the difference, that it has an implementation in the base class. You can override it in a derived class, but you don't have to! If you don't want to override it, the implementation of the base class will be used.

In this case, the implementation is the one provided in the Object class, which just prints out the class name. First, we override this implementation in the MyGeomObject. Go to the declaration of this class and put your cursor in the public section. Write "method " (with a trailing blank), then hit Ctrl+Space. IntelliSense will show you a list of virtual methods of the base class. Selected "ToString" and hit return. The declaration will automatically be added. Hit Ctrl+Shift+C to add the implementation.

The method body is just this line:

result := inherited + ' @ ('+ X.ToString+', '+Y.ToString+')';

New is the keyword inherited. With this keyword, you can call the ascendent method (in this case the ToString method in the Object class) and work with its result. The string returned from the new ToString method will have the form: "OOPSample.MyXXXX @ (23,42)", where the first part comes from the ascendant ToString method called via inherited.

Just try it and add some objects to the listbox, now. It will use our new ToString method!

To Override Or Not To Override

Of course we have to implement the ToString method in the descendant classes, too, because they can describe themselves even better using their additional properties like width, height or radius. Let's implement the method in the MyRectangle class.

Again, go the interface section of this class, type "method ", let the editor create the declaration and (Ctrl+Shift+C) the method body. But this time, remove the override keyword! We'll put it back there, soon, it's just for demonstration ;-)

The body of the ToString method in this class could look like this:

result := 'Rectangle @ ('+X.ToString+', '+Y.ToString+') : '+Width.ToString+' x '+Height.ToString;

Run the program again and add a rectangle to the listbox. And what you see is ... *drum roll* ... the old version from the MyGeomObject class! How is this happening? As you probably already guessed, it has to do with the removed override keyword.

The items in the listbox are all declared as Object, to make it possible, that the ListBox can hold all kinds of objects. Let's just write it down in some kind of pseudo-implementation that doesn't really exist:

var item : Object;

Now, add an object of class MyRectangle:

item := aRectangle;

When the listbox wants to get the string-representation, it calls the ToString method:

item.ToString;

Because the item variable is declared as Object, this is primary a call to the Object class' ToString method. This can be changed by overriding the method. Then the "new" ToString method is called.

What happens in our case, is, that the MyGeomObject class has overridden the ToString method (that's why this one is called), but the MyRectangle class has not overridden the existing method, but introduced a new one (that's why it is not called). The compiler even warns you about this: "(PW2) Method "ToString" hides a parent method".


Perhaps it's a good time to do a short summary:


Now that the demonstration is over, you can re-add the override keyword and be happy about the correct presentation in the listbox ;-) Oh, and of course do the same thing for the MyCircle class!

In A Nutshell: Records

Besided classes there're so called records. In .NET they provide most of the functionality classes provide. They have members with different visibilities, they have methods and they have properties and fields.

The first big difference is, that they aren't reference types but value types! They behave like, for example, Integer. You don't have to instantiate a record (though you can provide a constructor if you want to). And if you assign one record type variabel to another, a copy is created!

That can be a advantage, when this behaviour is more logical than the behaviour of a reference type. For example when you want to work with complex numbers, you would expect them to behave like all other numbers - like value types. Often it's a disadvantage, because with exceptions of var- and out-parameters, you cannot get the reference to a record. With every assignment it will be completely duplicated. That can cost very much time when it happens a lot.

Second difference is, that they don't support inheritance. You can't inherit from a record and a record cannot inherit from another type than Object. Inheriting from Object means, that you can override the ToString method. Additionally a record can implement interfaces (see right below).

You declare a record just like a class, just use the record keyword instead of the class keyword.


Interfaces

An interface defines a set of sigantures of methods and properties. For properties it defines the name and if the property has a getter and / or setter, in case of methods it defines the name, the parameters and the return type. A class then can implement this interface and then has to implement all these methods and properties.

For example let's assume we want to implement the feature for drawing our geometrical objects, but for now only in the MyRectangle class. That makes it impossible to use an abstract method in the MyGeomObject class, because then this method would have to be implemented in the MyCircle class, too.

Therefore we create an interface. Please add a new item to the project, but this time not a class but an interface. Call it (and the file) "IDrawableObject". Interfaces are always prepended with a capital "i".

The new interface defines one method:

method DrawOn(aBmp : Bitmap);

Make sure, that you include the namespace "System.Drawing" into the uses-clause in the IDrawableObject.pas file. That's where the Bitmap class is in. This class represents a Bitmap image in the computer's memory, which then can be manipulated, displayed or saved to a file. When a class implements the IDrawableObject interface, it will draw the geometric form it stands for onto this bitmap.

Implementing an Interface

That's what we have to implement in the MyRectangle class, now. Change the declaration of this class to this:

MyRectangle = public class(MyGeomObject, IDrawableObject)

This time you can use the smart tag under "IDrawableObject" to automatically implement the interfaces' members.

Notice, that you not only can derive a class from another and at the same time implement an interface, but you can implement as many interfaces as you want! Just write them into the class declaration, separated by commas. The only thing to you have to look after, is, that if you derive your class from another, the ancestor class has to be the first item in the list!

Now go to the implementation of the DrawOn method. It will have only these five lines:

if aBmp = nil then
 exit;

using gr := Graphics.FromImage(aBmp) do
 gr.DrawRectangle(Pens.Black, X,Y,Width,Height);

In the first two lines, it is checked, if the Bitmap was created. If it is not, the parameter will be nil and the method will exit without drawing anything.

(The following paragraph will be too short to understand the matter completely, but it should be enough for now.)

The fourth line introduces a new statement: The using-block. It creates a new variable gr of type Graphics (type infered), which comes from the class method Graphics.FromImage. The scope of this variable is limited to the code inside the using-block. The using statement will make sure, that when it has finished, all resources used by gr will be freed, even if an error occured during the execution of the block. That is important, because the Graphics class uses unmanaged resources that can't be freed automaitcally by the .NET framework.

The fifth line is again very easy to understand. It calls a method of the gr object in order to draw a rectangle on the bitmap. The Graphics provied many methods to make working with bitmaps easier. The method takes five parameters. The first parameter defines a pen (we use a pre-defined, black pen here), the other four parameters define position and size of the rectangle to draw.

Well, and then we're finished. The method drew a rectangle on the bitmap by using the Graphics class, which provides methods for that.

Who Do You Implement?

To actually get a picture of the geometrical forms, we will have to call the DrawOn method for every item in the list of geometrical obejcts - but only on every item which implements the IDrawableObject interface!

Folded Code Region

First add a new private field to the MainForm class: fBitmap : Bitmap. That will be the bitmap the forms are drawn on, but of course it has to be created, first. That can be done in the constructor of the mainform. You find it, when you click on the "+" left of the gray text "Contruction and Dispostion". That is a folded code region, which you unfold by clicking on the "+".

Find the line InitializeComponent() in the constructor and add after (where a comment tells you, that you can add your own code there ;-)) it the line:

fBitmap := new Bitmap(pbCanvas.Width, pbCanvas.Height);

That creates a new bitmap of exactly the size of the picturebox.

Then create a new click handler for the "Draw" button (by double clicking on it).

using gr := Graphics.FromImage(fBitmap) do
  gr.Clear(Color.White);

for g : MyGeomObject in lbGeomtricalObjects.Items do
  if g is IDrawableObject then
    IDrawableObject(g).DrawOn(fBitmap);

pbCanvas.Image := fBitmap;

First we have to clear the bitmap, by filling it with white color (lines one and two).

Then we loop through the items in the listbox and if it implements the IDrawbaleObject interface, we call the DrawOn method (next three non-blank lines).

This time we can't use type inference for the loop variable, because the items of the listbox are stored as Objects and could be simply anything. We have to specifiy the type explicitely. The checking, if an item implements the interface, is done by using the is operator. To call the DrawOn method, we first have to cast the item into the interface.

Last thing to do, is, to tell the picturebox that it has to display our bitmap (last line).

You now should be able to run the program, add a rectangle to the list and then hit the draw button :-)

matching

The code would be much nicer, if we could declare the loop variable g as IDrawableObject. But this would create an error as soon as a circle is in the list, because MyCircle does not implement IDrawableObject.

But Delphi Prism wouldn't be Delphi Prism if it hadn't a feature for this: for matching!

for matching g : IDrawableObject in lbGeomtricalObjects.Items do
  g.DrawOn(fBitmap);

Using this keyword in the for-loop tells the compiler to just use the items that actually have the matching type. For all other items the loop body won't be executed. And because we now have declared g as IDrawableObject, we don't need to cast it anymore. :-)

Importance of Interfaces

The importance of interfaces in .NET can't be accented enough. For example the using-statement works with classes that implement the IDisposable-interface. Thus it works with every class that implements that, which is only a minimal requirement. That's one of the great benefits of interfaces: They only demand the few methods that are really needed at a point and nothing more. They dont't make any further demand to the class, as for example a common base class or something like that.

For example there're list-classes (we'll learn about in the next chapter), that can store any class and sort it - if it implements the IComparable-interface. Or binding between GUI and classes (perhaps sometimes later in a addition to this article) works using the INotifyPropertyChanged-interface.

So, it's important that you get familiar with working with interfaces and next time, you design a class structure, think about the use of interface for your own purposes.


See Also

The Prism Primer: Part 1Part 2Part 3Part 4Part 5Part 6Part 7Part 8Part 9


Oxygene-48.png

Oxygene

GlossariesKeywordsTypesFAQHow To

Navigation
Areas
More
Toolbox