Programming Guide:xppguide

Displaying animated bitmaps Foundation

This section discusses a variety of multi-threading issues using animation as an example. Animated graphic images have become popular because they attract the user's attention and provide for some kind of entertainment. This can be advantageous but can also have the opposite effect of distracting the user from their original task while using the software. Just as in real life, the rule "too many is too much" applies to animated images as well, and the technique should be used sparsely in an application. If used in the right places, however, animated images can be very informative for the user. So every developer should know how animated images are programmed and how this is achieved in the easiest way. Besides this, animation is a perfect topic to highlight different aspects of a multi-threaded program.

The fundamental technique

An animation consists of a series of single images each of which shows a distinct phase of the animation. We are using bitmaps for the animation and the first thing to start with is creating the bitmaps. An example of a series of three bitmaps is shown below.

Three phases of an animation

A black and white circle rotated twice by 30 degrees is not very exciting but is a sufficient example to demonstrate animation and multi-threading. A user gets the impression of a rotating circle when the three bitmaps are displayed one after the other at the same place and that is basically the whole story of animation: displaying different images (or phases) at the same or changing position. Once the series of bitmaps is available, we need the following two classes to bring the animation to life:

XbpBitmap;XBPBITMAPF__

One XbpBitmap object is required to load one bitmap file and display its contents. We will create three of these objects for a three-phased animation and collect them in an array for easy access.

XbpStatic;XBPSTATICF__

An XbpStatic object is used as a kind of canvas where the bitmaps are drawn. It provides for the presentation space required by XbpBitmap objects when displaying the image.

Knowing these two classes we can put the pieces together and discuss the basic technique for an animation using a simple program:

01: PROCEDURE Main 
02:    LOCAL oXbp, aBitmaps 
03: 
04:    Setcolor( "N/W" ) 
05:    CLS 
06: 
07:    oXbp     := XbpStatic():new( ,, {10,300}, {44,44} ) 
08:    oXbp:create() 
09: 
10:    aBitmaps := PrepareAnimation( { "Phase1.bmp", ; 
11:                                    "Phase2.bmp", ; 
12:                                    "Phase3.bmp"  } ) 
13:    DO WHILE .T. 
14:       Animate( oXbp, aBitmaps ) 
15:       Sleep( 10 ) 
16:    ENDDO 
17: RETURN 
18: 
19: 
20: // Loads bitmap files 
21: FUNCTION PrepareAnimation( aFiles ) 
22:    LOCAL i, imax := Len( aFiles ) 
23:    LOCAL aBitmaps:= Array( imax ) 
24: 
25:    FOR i:=1 TO imax 
26:       aBitmaps[i] := XbpBitmap():new():create() 
27:       aBitmaps[i]:loadFile( CompleteFileName( aFiles[i] ) ) 
28:    NEXT 
29: RETURN aBitmaps 
30: 
31: 
32: // Displays a collection of bitmaps 
33: PROCEDURE Animate( oXbp, aBitmaps ) 
34:    STATIC nCurrent := 0 
35:    LOCAL oPS := oXbp:lockPS() 
36: 
37:    nCurrent ++ 
38:    IF nCurrent > Len( aBitmaps ) 
39:       nCurrent := 1 
40:    ENDIF 
41: 
42:    aBitmaps[ nCurrent ]:draw( oPS, {1,1} ) 
43:    oXbp:unlockPS() 
44: RETURN 
45: 
46: 
47: // Iterate the XPPRESOURCE environment variable for the file 
48: FUNCTION CompleteFileName( cFileName ) 
49:    LOCAL nAt, cPath, cEnvPath 
50: 
51:    cEnvPath := GetEnv( "XPPRESOURCE" ) 
52:    DO WHILE .NOT. Empty( cEnvPath ) 
53:      nAt := At( ";", cEnvPath ) 
54:      cPath    := AllTrim( Left( cEnvPath, nAt-1 ) ) 
55:      IF File( cPath + [\] + cFileName ) 
56:        RETURN( cPath + [\] + cFileName ) 
57:      ENDIF 
58:      cEnvPath := AllTrim( SubStr( cEnvPath, nAt+1 ) ) 
59:    ENDDO 
60: RETURN "" 

The bitmap files participating in the animation are loaded into the program by XbpBitmap objects in a separate function PrepareAnimation(). The function receives an array of bitmap file names and creates for each file an XbpBitmap object in line #26 which in turn loads the bitmap file. The objects are collected in an array which is returned.

The animation is executed by calling procedure Animate() continuously within a DO WHILE loop (line #14). The procedure receives the XbpStatic object as the place where to draw the bitmaps, and the array containing XbpBitmap objects which know how to draw a bitmap. A bitmap becomes visible on the screen in line #42 where the :draw() method of an XbpBitmap object is called. Once a bitmap is drawn, the program pauses for 0.1 seconds in line #15 (the unit for the Sleep() function is one hundredth of a second).

The key for the animation is the variable nCurrent. It is declared as STATIC (line #34) and retains its value when procedure Animate() returns. The only thing necessary for displaying the next bitmap of the animation is, therefore, to increment the STATIC variable and reset it to One if its value exceeds the number of available bitmaps (line #37 through #40). As a result, each call to procedure Animate() displays another bitmap.

This program demonstrates the basic techniques required for programming an animation: one XbpBitmap object is created for each phase of the animation. Each XbpBitmap object loads a single bitmap file and draws the image in another Xbase Part. This Xbase Part must know the method :lockPS() which returns a presentation space required by an XbpBitmap object for drawing its bitmap. The program uses an XbpStatic object as a canvas but it could be any Xbase Part derived from XbpWindow() (:lockPS() is a method in the XbpWindow() class that is inherited by any object derived from XbpWindow(), such as XbpStatic()). You can draw an animation in a pushbutton, for example, when you use an XbpPushbutton object instead of an XbpStatic object. If you change XbpStatic() in line #7 to XbpPushbutton(), you see the animation within a pushbutton. So, it lies within your imagination how to use this technique in your applications.

Identifying program code for a thread

The example program cannot be used for anything but loading and displaying bitmaps. As a matter of fact, the program cannot be stopped unless you press Alt+C. So, what is its purpose in the multi-threading area? The answer lies in the question: Why is the program code separated into PrepareAnimation() and Animate()? The whole animation could have been programmed in Main(). To find the answer, take the program logic point of view and think what makes PrepareAnimation() logically different from Animate()? The difference is that PrepareAnimation() is called once while Animate() is called multiple times, and this is how the example program is structured: The part which needs to be called once is separated entirely from the part that must be called multiple times. This again leads to a key question you have to answer when using multi-threading: how often is a procedure or function called? Answering this "key question" will help to structure your multi-threaded programs.

To make the example a bit more useful we will allow for user input while bitmaps are being displayed. The easiest way to accomplish this is @..SAY..GET followed by the READ command, which is as good as any other approach for obtaining user input in this discussion. We add a new level of complexity to the program and it consists now of two completely different things:

User input                     Animation

@ 10, 10 SAY "X" GET varX      DO WHILE .T. 
@ 12, 10 SAY "Y" GET varY         Animate( oXbp, aBitmaps ) 
                                  Sleep( 10 ) 
READ                           ENDDO 

This is an ideal situation: something requires user input and something else does not. In fact, an animation must run independently of user interaction and this makes it a perfect candidate for a separate thread. The main thread allows for data entry in the program while a second thread is busy with displaying bitmaps. The second thread, however, will execute only that part of the animation which must be executed repeatedly.

Starting a thread

Once the program code that can be run in a separate thread is identified, we can encapsulate it in a procedure and let a Thread object handle the program execution. A Thread object represents an additional thread, or execution path, so that two procedures can be executed at the same time. This requires only few modifications in our example program:

01: // User enters data in Main 
02: PROCEDURE Main 
03:    LOCAL oXbp, aBitmaps, oThread 
04:    LOCAL cFirst := "Henry  ", cLast := "Miller "
05: 
06:    Setcolor( "N/W,W+/B" ) 
07:    CLS 
08: 
09:    oXbp     := XbpStatic():new( ,, {10,300}, {44,44} ) 
10:    oXbp:create() 
11: 
12:    aBitmaps := PrepareAnimation( { "Phase1.bmp", ; 
13:                                    "Phase2.bmp", ; 
14:                                    "Phase3.bmp"  } ) 
15: 
16:    oThread := Thread():new()
17:    oThread:start( "ExecuteAnimation", oXbp, aBitmaps )
18: 
19:    SET CURSOR ON
20:    @ 10, 10 SAY "Firstname:" GET cFirst
21:    @ 12, 10 SAY " Lastname:" GET cLast
22:    READ
23: RETURN 
24: 
25: 
26: // This runs in a separate thread 
27: PROCEDURE ExecuteAnimation( oXbp, aBitmaps )
28:    DO WHILE .T. 
29:       Animate( oXbp, aBitmaps ) 
30:       Sleep( 10 ) 
31:    ENDDO 
32: RETURN 
33: 
34: 
35: // Loads bitmap files 
36: FUNCTION PrepareAnimation( aFiles ) 
37:    LOCAL i, imax := Len( aFiles ) 
38:    LOCAL aBitmaps:= Array( imax ) 
39: 
40:    FOR i:=1 TO imax 
41:       aBitmaps[i] := XbpBitmap():new():create() 
42:       aBitmaps[i]:loadFile( CompleteFileName( aFiles[i] ) ) 
43:    NEXT 
44: RETURN aBitmaps 
45: 
46: 
47: // Displays a collection of bitmaps 
48: PROCEDURE Animate( oXbp, aBitmaps ) 
49:    STATIC nCurrent := 0 
50:    LOCAL  oPS 
51: 
52:    nCurrent ++ 
53:    IF nCurrent > Len( aBitmaps ) 
54:       nCurrent := 1 
55:    ENDIF 
56: 
57:    oPS := oXbp:lockPS() 
58:    aBitmaps[ nCurrent ]:draw( oPS, {1,1} ) 
59:    oXbp:unlockPS() 
60: RETURN 
61 
62 
63: // Iterate the XPPRESOURCE environment variable for the file 
64: FUNCTION CompleteFileName( cFileName ) 
65:    LOCAL nAt, cPath, cEnvPath 
66: 
67:    cEnvPath := GetEnv( "XPPRESOURCE" ) 
68:    DO WHILE .NOT. Empty( cEnvPath ) 
69:      nAt := At( ";", cEnvPath ) 
70:      cPath    := AllTrim( Left( cEnvPath, nAt-1 ) ) 
71:      IF File( cPath + [\] + cFileName ) 
72:        RETURN( cPath + [\] + cFileName ) 
73:      ENDIF 
74:      cEnvPath := AllTrim( SubStr( cEnvPath, nAt+1 ) ) 
75:    ENDDO 
76: RETURN "" 

The effect of this program is that a user can enter data while three bitmaps are continuously displayed in a round robin scheme. The code for loading and displaying the bitmaps is not listed here because it is the same as discussed in The fundamental technique. The important changes are:

The Main() procedure covers user interaction and allows for data entry using the READ command in line #22.
The DO WHILE loop is moved to a separate procedure (line #28 through line #31), so that it can be executed by the Thread object created in line #16.

The example program consists now of two threads and both execute program code performing two completely different tasks: data entry versus display of bitmaps. This situation is perfect for multi-threading since both tasks have nothing in common. The two threads use different memory variables and different code which is the best (or easiest) situation a programmer can have in multi-threading. The only task required is creating a Thread object and telling it what program code to execute. This is done by calling the :start() method (line #17) which receives as first parameter the name of the function/procedure to be executed in the new thread. All following parameters passed to :start() are just passed on to the called procedure.

Since the program flow is not obvious from the program code it must be emphasized that the DO WHILE loop is executed at the same time as the READ command. This is something the operating system takes care of and clearly reveals the nature of multi-threading. It also shows the superiority of the multi-threaded approach over a single-threaded solution. It is possible to display bitmaps every 0.1 seconds while READ is executed in a single-threaded application. It is impossible, however, to program this with less code. One would have to hook into the Get system using a customized Get reader or would need to modify the Get system accordingly. Both approaches result in an unnecessary programming overhead and would create a logical dependency between two tasks which don't have anything in common.

Stopping a thread

Although the example program is now capable of performing two tasks simultaneously (user input and animation) it has a major disadvantage: the thread displaying the animation cannot be stopped when the READ command is finished. This does not matter in the example program because the READ command is followed by the RETURN statement which ends the entire program, including the second thread. But what if some other code would follow the READ command? The animation would continue to run and there is absolutely no way to stop the thread displaying bitmaps because of the DO WHILE .T. condition. This loop runs forever and we must find a way to stop the animation, or thread.

Stopping a thread is not as simple as starting it because a Thread object does not have a :stop() method, there is only a :start() method. A Thread object terminates its thread automatically if the code has run to completion in the thread. In other words, if a RETURN statement is executed in that part of a program which is invoked via the :start() method.

The RETURN statement is never reached in procedure ExecuteAnimation() of the example. The only possibility for this is to exit the DO WHILE .T. loop. This could be achieved by using a PUBLIC variable serving as logical condition for the loop. The variable would have to be PUBLIC because it must be visible in two threads. The first thread would set this variable to .T. before the second thread starts, and would set it to .F. in order to exit the loop. This in turn would cause the second thread to terminate.

At first thought, this is a feasible scenario but there is a more elegant solution which uses a special feature of the Thread object. Have a look at the modified example program below which uses special features of a Thread object:

01: // User enters data in Main 
02: PROCEDURE Main 
03:    LOCAL oXbp, aBitmaps, oThread 
04:    LOCAL cFirst := "Henry  ", cLast := "Miller " 
05: 
06:    Setcolor( "N/W,W+/B" ) 
07:    CLS 
08: 
09:    oXbp     := XbpStatic():new( ,, {10,300}, {44,44} ) 
10:    oXbp:create() 
11: 
12:    aBitmaps := PrepareAnimation( { "Phase1.bmp", ; 
13:                                    "Phase2.bmp", ; 
14:                                    "Phase3.bmp"  } ) 
15: 
16:    oThread := Thread():new() 
17:    oThread:setInterval( 10 )
18:    oThread:start( "Animate", oXbp, aBitmaps ) 
19: 
20:    SET CURSOR ON 
21:    @ 10, 10 SAY "Firstname:" GET cFirst 
22:    @ 12, 10 SAY " Lastname:" GET cLast 
23:    READ 
24: 
25:    oThread:setInterval( NIL )
26:    oThread:synchronize( 0 )
27: 
28:    WAIT "Thread has stopped"
29: RETURN 
30: 
31: 
32: // Displays a collection of bitmaps 
33: PROCEDURE Animate( oXbp, aBitmaps ) 
34:    STATIC nCurrent := 0 
35:    LOCAL oPS := oXbp:lockPS() 
36: 
37:    nCurrent ++ 
38:    IF nCurrent > Len( aBitmaps ) 
39:       nCurrent := 1 
40:    ENDIF 
41: 
42:    aBitmaps[ nCurrent ]:draw( oPS, {1,1} ) 
43:    oXbp:unlockPS() 
44: RETURN 
45: 
46: 
47: // Loads bitmap files 
48: FUNCTION PrepareAnimation( aFiles ) 
49:    LOCAL i, imax := Len( aFiles ) 
50:    LOCAL aBitmaps:= Array( imax ) 
51: 
52:    FOR i:=1 TO imax 
53:       aBitmaps[i] := XbpBitmap():new():create() 
54:       aBitmaps[i]:loadFile( CompleteFileName( aFiles[i] ) ) 
55:    NEXT 
56: RETURN aBitmaps 
57: 
58: 
59: // Iterate the XPPRESOURCE environment variable for the file 
60: FUNCTION CompleteFileName( cFileName ) 
61:    LOCAL nAt, cPath, cEnvPath 
62: 
63:    cEnvPath := GetEnv( "XPPRESOURCE" ) 
64:    DO WHILE .NOT. Empty( cEnvPath ) 
65:      nAt := At( ";", cEnvPath ) 
66:      cPath    := AllTrim( Left( cEnvPath, nAt-1 ) ) 
67:      IF File( cPath + [\] + cFileName ) 
68:        RETURN( cPath + [\] + cFileName ) 
69:      ENDIF 
70:      cEnvPath := AllTrim( SubStr( cEnvPath, nAt+1 ) ) 
71:    ENDDO 
72: RETURN "" 

The solution is that a DO WHILE loop is not required at all when program code must be executed repeatedly in a thread. This is an important shift in program logic and becomes possible due to a Thread object's intelligence. The key to the program logic is line #17 where a time interval of 10 hundredths of a second (0.1 seconds) is set for the Thread object to restart the Animate() procedure. This procedure is executed in the new thread and called for the first time in line #18. Once the procedure is finished, it is restarted automatically after 0.1 seconds by the Thread object. This means that program code is executed repeatedly which runs parallel to the READ command:

From the program logic point of view it is important to understand that while the READ command is executed in thread A (line #23) the Animate() procedure is executed entirely in thread B from line #33 down to line #44. The RETURN statement is really executed in thread B but this does not end the thread. Instead, thread B is just halted for 0.1 seconds due to the time interval set. It resumes with executing Animate() again when the interval has elapsed. The thread consumes no system resources while it pauses, it is -literally spoken- put to sleep for 0.1 seconds.

The :setInterval() method of the Thread object is the easiest way to achieve repeated execution of the same program code in a thread, once a thread is started. This again shows a major difference in program logic compared to single-threaded programs: Instead of using a DO WHILE loop for code repetition, a time interval is defined which causes the Thread object to execute program code again when the time interval has elapsed. Of course, we could use a DO WHILE loop as well, but there is one big advantage using the :setInterval() approach: the time interval can be voided and this is our chance to stop a thread easily:

25:      oThread:setInterval( NIL ) 
26:      oThread:synchronize( 0 ) 

These two lines allow you to effectively stop thread B from thread A because thread B does not execute the Animate() procedure again when the interval is set to NIL. The :synchronize() method accepts as a parameter a time-out value. Passing the value zero to this method means: there is no time-out condition. This causes thread A to wait forever until thread B has ended. This again is the only way to be sure that thread B is no longer running and only then may thread A resume with program execution.

Calling the :synchronize() method in line #26 makes sure that thread A waits until thread B has terminated. This is something you must be aware of when using multiple threads. If you want to stop a thread you have to assure in your program that the thread you want to stop has definitely ended. Otherwise you will have a good chance of getting inconsistent runtime errors in your multi-threaded programs. One time your application bombs, or ends unexpectedly, but when you restart it to find the error it just runs fine. Such an occasional runtime error is a worst case scenario in multi-threaded programs and you are well advised to avoid this kind of problem right from the beginning. You should keep in mind, therefore, that "stopping a thread" means be sure that the thread has ended .

Making it thread-safe

The example program is now in a stage where we can start an animation, retrieve user input while the animation is running, stop the thread and restart it if this is necessary. However, there is still one major design flaw that makes the example unsuitable for reuse. Just recall how the animation is displayed:

01: // Displays a collection of bitmaps 
02: PROCEDURE Animate( oXbp, aBitmaps ) 
03:    STATIC nCurrent := 0
04:    LOCAL oPS := oXbp:lockPS() 
05: 
06:    nCurrent ++ 
07:    IF nCurrent > Len( aBitmaps ) 
08:       nCurrent := 1 
09:    ENDIF 
10: 
11:    aBitmaps[ nCurrent ]:draw( oPS, {1,1} ) 
12:    oXbp:unlockPS() 
13: RETURN 

Since the Animate() procedure is entirely executed before it is called again, the usage of a STATIC variable for tracing the current bitmap of the animation is obvious at first sight. A STATIC variable retains its last value and we get the next bitmap to be displayed in a new execution cycle by simply incrementing the STATIC variable nCurrent in line #6. This is an absolutely correct implementation and a sound program logic as long as there is only one animation running.

But what happens if two animations are displayed using two threads? The STATIC variable nCurrent would be incremented alternating from two threads and this would spoil both animations since the sequence of bitmap display is not guaranteed in either case. So, this implementation is not thread-safe because procedure Animate() is not re-entrant. It cannot be called simultaneously from more than one thread.

This situation is something you will most probably run into if you are not familiar with multi-threading, and you must be aware of it! Remember the key question: How often is a procedure or function called? This question includes not only how often something is called in one thread, but also How many threads execute the same code simultaneously? If a program code runs perfectly in one thread, it does not mean that it will work simultaneously in many threads. Take a look back to the discussion in the The fundamental technique section where the STATIC variable nCurrent is said to be "the key" for an animation. In fact, using a STATIC variable is a great idea from the "animation" point of view, but when we look at the program logic from the "multi-threading" point of view, we have to come up with a better idea.

The solution for the problem is to program only functions etc. which are re-entrant. This means that the value of variables a function relies on must not be changed in different threads at the same time. STATIC variables are in most cases unsuitable for multi-threading and we have to change the implementation of the Animate() procedure. The easiest way to replace the variable nCurrent is the array aBitmaps. The problem is solved when the array contains not only XbpBitmap objects but also the array index pointing to the current bitmap:

{ Array index, { XbpBitmap1, XbpBitmap2, XbpBitmap3 } } 

Using an array of this structure, the Animate() procedure becomes thread-safe and looks as follows:

01: #define BMP_IDX 1
02: #define BITMAP  2
03: 
04: // User enters data in Main 
05: PROCEDURE Main 
06:    LOCAL oXbp, aAnimContext, oThread 
07:    LOCAL cFirst := "Henry  ", cLast := "Miller " 
08: 
09:    Setcolor( "N/W,W+/B" ) 
10:    CLS 
11: 
12:    oXbp     := XbpStatic():new( ,, {10,300}, {44,44} ) 
13:    oXbp:create() 
14: 
15:    aAnimContext := PrepareAnimation( { "Phase1.bmp", ; 
16:                                        "Phase2.bmp", ; 
17:                                        "Phase3.bmp"  } ) 
18: 
19:    oThread := Thread():new() 
20:    oThread:setInterval( 10 ) 
21:    oThread:start( "Animate", oXbp, aAnimContext ) 
22: 
23:    SET CURSOR ON 
24:    @ 10, 10 SAY "Firstname:" GET cFirst 
25:    @ 12, 10 SAY " Lastname:" GET cLast 
26:    READ 
27: 
28:    oThread:setInterval( NIL ) 
29:    oThread:synchronize( 0 ) 
30: 
31:    WAIT "Thread has stopped" 
32: RETURN 
33: 
34: // Displays a collection of bitmaps 
35: PROCEDURE Animate( oXbp, aAnimContext ) 
36:    LOCAL nBitmap, aBitmaps, oPS := oXbp:lockPS() 
37: 
38:    aAnimContext[BMP_IDX] ++ 
39:    IF aAnimContext[BMP_IDX] > Len( aAnimContext[BITMAP] ) 
40:       aAnimContext[BMP_IDX] := 1 
41:    ENDIF 
42: 
43:   nBitmap  := aAnimContext[BMP_IDX] 
44:   aBitmaps := aAnimContext[BITMAP] 
45:   aBitmaps[ nBitmap ]:draw( oPS, {1,1} ) 
46:   oXbp:unlockPS() 
47: RETURN 
48: 
49: 
50: // Loads bitmap files 
51: FUNCTION PrepareAnimation( aFiles ) 
52:    LOCAL i, imax := Len( aFiles ) 
53:    LOCAL aBitmaps:= Array( imax ) 
54: 
55:    FOR i:=1 TO imax 
56:       aBitmaps[i] := XbpBitmap():new():create() 
57:       aBitmaps[i]:loadFile( CompleteFileName( aFiles[i] ) ) 
58:    NEXT 
59: RETURN { 1, aBitmaps }
60: 
61: 
62: // Iterate the XPPRESOURCE environment variable for the file 
63: FUNCTION CompleteFileName( cFileName ) 
64:    LOCAL nAt, cPath, cEnvPath 
65: 
66:    cEnvPath := GetEnv( "XPPRESOURCE" ) 
67:    DO WHILE .NOT. Empty( cEnvPath ) 
68:      nAt := At( ";", cEnvPath ) 
69:      cPath    := AllTrim( Left( cEnvPath, nAt-1 ) ) 
70:      IF File( cPath + [\] + cFileName ) 
71:        RETURN( cPath + [\] + cFileName ) 
72:      ENDIF 
73:      cEnvPath := AllTrim( SubStr( cEnvPath, nAt+1 ) ) 
74:    ENDDO 
75: RETURN "" 

The result of this implementation is that all data required for an animation is stored in one array. Two animations, or threads, respectively, use two different arrays holding different data for each animation. This means that procedure Animate() can be called from different threads, but each thread uses its own array when Animate() is executed. The key for the program logic is now that each thread gets its own set of data because different arrays arrive in the parameter aBitmaps when the procedure is called simultaneously from multiple threads.

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.