Programming Guide:xppguide

Building an Animation class Foundation

The chapter focuses on the various aspects for defining a user-defined Thread classthat inherits the ability to manage a thread from the built-in Thread class. Inheriting from the Thread class is just as easy as from any other class. However, there are some implementational rules that must be followed for successfully using objects of a user-defined Thread class. The example of displaying animated bitmaps is discussed again, using an object-oriented approach.

The different pieces of code discussed in this chapter are put together to a complete sample at the end of the chapter.

Specifications

Before we begin to implement a class we have to find a name for it. A good one is Animation because this is a synonym for what objects of the class will do in general. The next step is to define what data an Animation object will have, or know, and what it will do with the data. You have a pretty good idea about this from the example program in the previous section and should try to define the specifications on your own. Each animation, for example, must maintain its own array index for selecting the current bitmap, each must have its own XbpBitmap objects and so on. Since different data is required in different threads it becomes obvious that we must tell a Thread object what data to use for the animation and store the corresponding values directly in the object. Now this is what instance variables are good for: holding the data required for processing an animation. Instance variables have a symbolic name used to access their value and it is a good idea to specify not only the required data but also the names of the corresponding instance variables.

Data is used in the methods of an object and the different stages of an animation must be implemented in different methods. The entire animation is split into a number of methods, which also have symbolic names. The following tables show both the name and description of member variables (data) and methods (functionality) for the Animation class.

Specification of an Animation class

Data (Instance variables) for an Animation object (what it knows)
MemberVar/Method Description
:aSource Array holding names of bitmap files or their resource IDs if a resource file is used
:cDllName Name of DLL if a resource file is linked to a DLL
:aBitmaps Array holding XbpBitmap objects
:nTotal Total number of bitmaps for the animation
:nCurrent Array index pointing to the current bitmap
:aRect Coordinates where to draw the bitmaps in an Xbase Part

Functionality (Methods) of an Animation object (what it does)
MemberVar/Method Description
:init() Creates a new thread
:atStart() Loads bitmap files when the thread starts
:execute() Displays bitmap files while the thread is running
:atEnd() Releases bitmaps before the thread ends
:stop() Stops the animation

Inherited from the Thread class
MemberVar/Method Description
:start() Starts the animation

Pre-requisites for program logic

The specifications are detailed enough for declaring and implementing the Animation class. The key for this class to work properly is the built-in Thread class which enables objects of the Animation class to manage their own thread. As a matter of fact, each Animation object is a thread that knows how to display bitmaps. An Animation object uses data stored in member variables and processes them in its methods. For a better understanding of the implementation of the Animation class, let us first see how an object of this class is used:

01: // User enters data in Main 
02: PROCEDURE Main 
03:    LOCAL cFirst := "Henry  ", cLast := "Miller " 
04:    LOCAL oXbp, oThread 
05: 
06:    Setcolor( "N/W,W+/B" ) 
07:    CLS 
08: 
09:    oXbp := XbpStatic():new(,, {10,300},{44,44} ) 
10:    oXbp:create() 
11: 
12:   oThread := Animation():new( { CompleteFileName("Phase1.bmp" ), ;
13:                                 CompleteFileName("Phase2.bmp" ), ;
14:                                 CompleteFileName("Phase3.bmp" ) } )
15:    oThread:start( , oXbp )
16: 
17:    SET CURSOR ON 
18:    @ 10, 10 SAY "Firstname:" GET cFirst 
19:    @ 12, 10 SAY " Lastname:" GET cLast 
20:    READ 
21: 
22:    oThread:stop()
23: 
24:    WAIT "Thread has stopped" 
25: RETURN 

The effect of this program is the same as in the previous example program: a series of three bitmaps is shown in the XbpStatic object created in line #9 and #10, while the user can enter data due to the READ command in line #20. The object representing the second thread is referenced in a LOCAL variable which enables us to start and stop the thread as desired.

The usage of an Animation object is very simple. It is created in line #12 where an array of bitmap file names is passed to the :new()method. The animation is started in line #15 and stopped in line #22. That means: we need to call only three (!) methods in a program for effectively using an Animation object. These methods are :new(), :start() and :stop(). What happens with the member variables and other methods shown in the table listing the Specification of an Animation class? Well, they are used internally by the Animation object and provide for its "intelligence". There is something going on behind the scenes when calling :start() and :stop(). You cannot see this in the example code, but you must understand it for the implementation of the Animation class:

When an Animation object executes the :start() method, it loads the bitmaps when thread B begins with executing program code or displaying bitmaps. The bitmaps are displayed in a round robin scheme while thread B is running and they are released before thread B terminates.

This circumscribes three vital stages of a thread that must be understood for successfully implementing user-defined Thread classes, such as the Animation class: a thread starts, it runs and it ends. The Thread class has three pre-defined methods that are to be used in user-defined Thread classes for the three stages:

:atStart()

This method is called once. It is called automatically before a thread runs the :execute() method. An Animation object uses the :atStart()method to create XbpBitmap objects which actually load the bitmap files required for the animation.

:execute()

This method may be called repeatedly as long as the thread runs. It is called automatically after :atStart() and implements the code which is executed in the new thread. An Animation object displays bitmaps in the :execute() method.

:atEnd()

This method is is called once. It is called automatically before a thread terminates code execution. An Animation object uses the :atEnd() method to release XbpBitmap objects and their system resources.

Creating an Animation object

Up to now you have seen the specification of the Animation class, how objects of this class can be used within a program and how the program flow looks like when an Animation object is active. You also know that the Xbase++ Thread class has three methods which are called implicitly when a thread is started, while it is running and when it ends. With this knowledge it should be easy to understand the implementation of the Animation class:

01: CLASS Animation FROM Thread 
02: PROTECTED: 
03: 
04:    // data required for displaying animated bitmaps 
05:    VAR cDllName, aSource 
06:    VAR aBitmaps, nCurrent, nTotal 
07:    VAR aRect 
08: 
09:    // overloaded methods 
10:    METHOD atStart, execute, atEnd 
11: 
12: EXPORTED: 
13:    // overloaded method 
14:    METHOD init 
15: 
16:    // new method in Animation class 
17:    METHOD stop 
18: ENDCLASS 
19: 
20: 
21: METHOD Animation:init( aSource, cDllName ) 
22:    SUPER:init() 
23:    ::aSource  := AClone( aSource ) 
24:    ::cDllName := cDllName 
25: RETURN self 

The class declaration defines all instance variables an Animation object has in addition to the ones available in the Thread class. The methods :atStart(), :execute(), :atEnd() and :init()must be declared again, although they do exist in the super class. These methods are called implicitly and implement code that makes an Animation object different from a Thread object. The init() method is called implicitly within :new(). Just remember how an Animation object is created:

12:    oThread := Animation():new( { "Phase1.bmp", ; 
13:                                  "Phase2.bmp", ; 
14:                                  "Phase3.bmp"  } ) 

The same parameters passed to :new() are passed on to :init() and that's how an Animation object obtains the "knowledge" which bitmaps participate in the animation. When an array is passed to a method and stored in an instance variable, it is always a good idea to make a copy of that array (line #23). The most important part of the :init() method, however, is line #22 where the super class's :init() method is called. An Animation object cannot start a thread without initializing its super class, it would create a runtime error instead. But everything is fine here, the Animation object is properly initialized and ready to run the new thread.

Running the animation

The thread is started by calling the :start() method inherited from the Thread class. When this method is invoked the following code is executed in the new thread. It brings the animation to life and displays the bitmaps.

28: METHOD Animation:atStart( oXbp, nInterval, aRect ) 
29:    LOCAL i 
30: 
31:    IF nInterval == NIL 
32:       nInterval := 10 
33:    ENDIF 
34: 
35:    IF aRect == NIL 
36:       aRect := oXbp:currentSize() 
37:       aRect := { 0, 0, aRect[1], aRect[2] } 
38:    ENDIF 
39: 
40:    ::aRect    := aRect 
41:    ::nCurrent := 0 
42:    ::nTotal   := Len( ::aSource ) 
43:    ::aBitmaps := Array( ::nTotal ) 
44: 
45:    FOR i:=1 TO ::nTotal 
46:       ::aBitmaps[i] := XbpBitmap():new():create() 
47:       IF Valtype( ::aSource[i] ) == "N" 
48:          ::aBitmaps[i]:load( ::cDllName, ::aSource[i] ) 
49:       ELSE 
50:          ::aBitmaps[i]:loadFile( ::aSource[i] ) 
51:       ENDIF 
52:    NEXT 
53: 
54:    ::setInterval( nInterval ) 
55: RETURN self 
56: 
57: 
58: METHOD Animation:execute( oXbp ) 
69:    LOCAL oPS := oXbp:lockPS() 
60: 
61:    ::nCurrent ++ 
62:    IF ::nCurrent > ::nTotal 
63:       ::nCurrent := 1 
64:    ENDIF 
65: 
66:    ::aBitmaps[ ::nCurrent ]:draw( oPS, ::aRect ) 
67:    oXbp:unlockPS() 
68: RETURN self 

The :atStart() method provides for all resources required by the :execute() method to run successfully. The XbpBitmap objects, for example, are created in line #46 and load the bitmaps in turn so that they are available when:execute()draws the images in line #66. Also, the time interval for an automatic repetition of :execute() is set in line #54 when the thread begins to run. It defaults to 10 hundredths of a second (line #32) and causes the Animation object to "sleep" for this period of time before calling :execute() again. This program logic is the same as we have discussed in the previous section. The only difference is how the thread is started:

1st try:    oThread:start( "Animate", oXbp, aBitmaps ) 

2nd try:    oThread:start( , oXbp ) 

When using the built-in Thread class, the name of the procedure a Thread object excutes in the new thread is passed to the :start() method. This is not the case in user-defined Thread classes since the code for the new thread is implemented in the :execute() method, which is reserved for this very purpose. All but the first parameters received by the :start() method are passed also to :atStart(), :execute()and :atEnd(). The Animation object knows where to draw the bitmaps because the parameter oXbp references an XbpStatic object, or Xbase Part. This parameter is passed to :execute() each time the method is invoked. So this is something to consider for the class design of user-defined Thread classes: We could store the Xbase Part in an instance variable but this is not necessary since it remains accessible via the parameter list. This approach has the additional advantage that there is less code to write for the clean-up at the end of a thread.

Memory issues

Cleaning up memory is good programming practice once you enter the multi-threading business. A perfect time for doing a clean-up is when a thread ends and that is exactly what the :atEnd() method does:

71: METHOD Animation:atEnd( oXbp ) 
72:    AEval( ::aBitmaps, {|o| o:destroy() } ) 
73:    oXbp:invalidateRect( ::aRect ) 
74:    ::aBitmaps := NIL 
75:    ::aRect    := NIL 
76: RETURN self 

In line #72 system resources allocated by XbpBitmap objects are released. Then the Xbase Part oXbp is informed that the rectangle on the screen occupied by the bitmaps is no longer valid (line #73) - which causes a screen update. Finally, all array references created in the :atStart() method are destroyed by assigning NIL to the corresponding instance variables.

Although memory issues are nothing you are bothered with in Xbase++ you should follow these rules when you define your own Thread classes:

Implement an :atEnd() method that does a clean-up.
Call the :destroy() method for all Xbase Parts that become obsolete or unaccessible (PROTECTED instance variables!) when the thread has ended.
Assign NIL to all instance variables that contain references to arrays, code blocks or objects when they become obsolete or unaccessible.
Stopping the animation

We have discussed now the code that runs in the thread created by an Animation object. The code implemented in the :execute() method is repeated forever unless we tell the object to terminate the thread. This is done in the method :stop() whose code is listed below:

80: METHOD Animation:stop 
81:    ::setInterval( NIL ) 
82:    ::synchronize( 0 ) 
83: RETURN self 

The method consists effectively of two lines of code (line #81 and #82) which make it both simple to implement and hard to understand if you are not familiar with multi-threaded program logic. However, it is imperative for you to understand these two lines of code when you intend to implement your own Thread classes. To stop a thread is a vital issue in multi-threaded programs but you have absolutely no control over the time when a thread ends because this lies within the responsibility of the operating system. There are different strategies how to stop a thread and you have to implement different code depending on how your Thread class runs, better to say: how the :execute() method is implemented.

An Animation object relies on a time interval being set for repeated execution of the :execute() method and this makes it easy to stop the thread. When the time interval is set to NIL (line #81), the :execute() method is not called again and the thread ends.

We have already discussed that :synchronize( 0 ) causes one thread to halt until another thread has ended. However, calling this method from within a user-defined Thread class makes line #82 really hard to digest and you ought to recall the situation when the :stop() method is executed. We have two threads running, A and B. Thread A knows the Animation object that represents thread B. Thread A stops thread B and must wait until thread B has actually ended:

The :stop() method is called in thread A which implies that the code of this method is also executed in thread A. From this it becomes obvious that ::synchronize( 0 ) (line #82) is executed in thread A and that it is thread A which is "synchronized" with thread B. During the synchronization, the :execute() method runs to completion and the :atEnd() method is executed before thread B finally ends:

The point of this discussion is to make it clear that a Thread object will always have methods that are executed in two different threads. The :stop() method, for example, is a method that cannot be executed in the thread represented by the Animation object. Just think what would happen when :stop() would be called inside the :execute() method? The thread would be synchronized with itself, or, in other words, it would be busy with waiting for its own end, which is a deadlock situation. As a matter of fact, this would be a major logical programming error and Xbase++ prevents you from a deadlock by raising a runtime error if your program runs into this situation.

The complete sample

Here you find the complete sample discussed in this chapter. You can copy the source from this place to paste it into your own prg file.

// User enters data in Main 
PROCEDURE Main 
   LOCAL cFirst := "Henry  ", cLast := "Miller " 
   LOCAL oXbp, oThread 

   Setcolor( "N/W,W+/B" ) 
   CLS 

   oXbp := XbpStatic():new(,, {10,300},{44,44} ) 
   oXbp:create() 

   oThread := Animation():new( { CompleteFileName("Phase1.bmp" ), ; 
                                 CompleteFileName("Phase2.bmp" ), ; 
                                 CompleteFileName("Phase3.bmp" ) } ) 
   oThread:start( , oXbp ) 

   SET CURSOR ON 
   @ 10, 10 SAY "Firstname:" GET cFirst 
   @ 12, 10 SAY " Lastname:" GET cLast 
   READ 

   oThread:stop() 

   WAIT "Thread has stopped" 
RETURN 


CLASS Animation FROM Thread 
PROTECTED: 

   // data required for displaying animated bitmaps 
   VAR cDllName, aSource 
   VAR aBitmaps, nCurrent, nTotal 
   VAR aRect 

   // overloaded methods 
   METHOD atStart, execute, atEnd 

EXPORTED: 
   // overloaded method 
   METHOD init 

   // new method in Animation class 
   METHOD stop 
ENDCLASS 


METHOD Animation:init( aSource, cDllName ) 
   SUPER:init() 
   ::aSource  := AClone( aSource ) 
   ::cDllName := cDllName 
RETURN self 


METHOD Animation:atStart( oXbp, nInterval, aRect ) 
   LOCAL i 

   IF nInterval == NIL 
      nInterval := 10 
   ENDIF 

   IF aRect == NIL 
      aRect := oXbp:currentSize() 
      aRect := { 0, 0, aRect[1], aRect[2] } 
   ENDIF 

   ::aRect    := aRect 
   ::nCurrent := 0 
   ::nTotal   := Len( ::aSource ) 
   ::aBitmaps := Array( ::nTotal ) 

   FOR i:=1 TO ::nTotal 
      ::aBitmaps[i] := XbpBitmap():new():create() 
      IF Valtype( ::aSource[i] ) == "N" 
         ::aBitmaps[i]:load( ::cDllName, ::aSource[i] ) 
      ELSE 
         ::aBitmaps[i]:loadFile( ::aSource[i] ) 
      ENDIF 
   NEXT 

   ::setInterval( nInterval ) 
RETURN self 


METHOD Animation:execute( oXbp ) 
   LOCAL oPS := oXbp:lockPS() 

   ::nCurrent ++ 
   IF ::nCurrent > ::nTotal 
      ::nCurrent := 1 
   ENDIF 

   ::aBitmaps[ ::nCurrent ]:draw( oPS, ::aRect ) 
   oXbp:unlockPS() 
RETURN self 


METHOD Animation:atEnd( oXbp ) 
   AEval( ::aBitmaps, {|o| o:destroy() } ) 
   oXbp:invalidateRect( ::aRect ) 
   ::aBitmaps := NIL 
   ::aRect    := NIL 
RETURN self 


METHOD Animation:stop() 
   ::setInterval( NIL ) 
   ::synchronize( 0 ) 
RETURN self 


// Iterate the XPPRESOURCE environment variable for the file 
FUNCTION CompleteFileName( cFileName ) 
   LOCAL nAt, cPath, cEnvPath 

   cEnvPath := GetEnv( "XPPRESOURCE" ) 
   DO WHILE .NOT. Empty( cEnvPath ) 
     nAt := At( ";", cEnvPath ) 
     cPath    := AllTrim( Left( cEnvPath, nAt-1 ) ) 
     IF File( cPath + [\] + cFileName ) 
       RETURN( cPath + [\] + cFileName ) 
     ENDIF 
     cEnvPath := AllTrim( SubStr( cEnvPath, nAt+1 ) ) 
   ENDDO 
RETURN "" 

Feedback

If you see anything in the documentation that is not correct, does not match your experience with the particular feature or requires further clarification, please use this form to report a documentation issue.