< Lesson 3: Close Window  |  Index  |  Lesson 5: OS4 Libraries >

Lesson 4:  Draw

This example is a simple drawing program.  It opens up a single window and allows the user to "paint" by pressing the left mouse button and moving the mouse around the interior of the window.  In this tutorial we will discuss the idea of procedural programming.  We will also introduce the several new IntuiMessage codes and data members as well as the use of function return values to report errors.  Finally I will explain the mysteries of signal bits and why we use the crazy code

1L << MyWindow->UserPort->mp_SigBit

to check for these signal bits.

001   #include <stdio.h>
002   #include <exec/types.h>
003   #include <graphics/gfx.h>
004   #include <intuition/intuition.h>
005   #include <dos/dos.h>
007   /* Prototypes for system functions. */
008   #include <proto/exec.h>
009   #include <proto/graphics.h>
010   #include <proto/intuition.h>
011   #include <proto/dos.h>
013   /* Prototypes for our functions. */
014   int program_init(void);
015   void program_loop(void);
016   void clean_up(void);
018   /* Global variables. */
019   struct GfxBase *GfxBase = NULL;
020   struct IntuitionBase *IntuitionBase = NULL;
021   struct Window *MyWindow = NULL;
023   int main(void)
024   {
025      int error;
027      error = program_init(); /* Open needed libraries and the main window. */
029      if ( !error )
030      {
031         program_loop(); /* If everything was initialized, execute the main program code. */
032      }
034      clean_up(); /* We must clean up before we exit. */
036      return 0;
037   }
040   int program_init(void)
041   {
042      int result = 0;
044      /* First open the graphics.library. */
045      GfxBase = (struct GfxBase *)OpenLibrary( "graphics.library", 0L );
047      if ( GfxBase != NULL)
048      {
049         /* Second open the intuition.library. */
050         IntuitionBase = (struct IntuitionBase *)OpenLibrary( "intuition.library", 0L );
052         /* Was the library opened? */
053         if ( IntuitionBase != NULL )
054         {
055            /* Since the library was opened, we can open a window. */
056            MyWindow = (struct Window *)OpenWindowTags( NULL,
057                            WA_Left, 20,
058                            WA_Top, 20,
059                            WA_Width, 200,
060                            WA_Height, 200,
061                            WA_Title, (ULONG)"My Window",
062                            WA_DepthGadget, TRUE,
063                            WA_CloseGadget, TRUE,
064                            WA_SizeGadget, TRUE,
065                            WA_DragBar, TRUE,
066                            WA_GimmeZeroZero, TRUE,
067                            WA_ReportMouse, TRUE,
069                            TAG_END );
071            /* Was the window opened? */
072            if ( MyWindow == NULL )
073            {
074               /* The window was not opened so display a message. */
075               printf( "Unable to open the window!\n" );
076               result = -1;
077            }
078         }
079         else
080         {
081            /* The intuition.library was not opened so display a message. */
082            printf( "Unable to open the intuition.library!\n" );
083            result = -2;
084         }
085      }
086      else
087      {
088         /* The graphics.library was not opened so display a message. */
089         printf( "Unable to open the graphics.library!\n" );
090         result = -3;
091      }
093      return result;
094   }
097   void clean_up(void)
098   {
099      /* If the window is open, close it. */
100      if ( MyWindow != NULL )
101      {
102         CloseWindow( MyWindow );
103      }
105      /* If the intuition.library is open, close it. */
106      if ( IntuitionBase != NULL )
107      {
108         CloseLibrary( (struct Library *)IntuitionBase );
109      }
111      /* If the graphics.library is open, close it. */
112      if ( GfxBase != NULL )
113      {
114         CloseLibrary( (struct Library *)GfxBase );
115      }
117      return;
118   }
121   void program_loop(void)
122   {
123      ULONG signals;
124      ULONG window_signal;
125      struct IntuiMessage *message;
126      UWORD msg_code;
127      ULONG msg_class;
128      BOOL end = FALSE;
129      BOOL left_mouse_button = FALSE;
131      /* Define the window signal. */
132      window_signal = 1L << MyWindow->UserPort->mp_SigBit;
134      /* Main program loop. */
136      /* Wait until the close button is pressed. */
137      while ( !end && MyWindow )
138      {
139         signals = Wait( window_signal );
141         /* Check the signal bit for our message port. Will be true if these is a message. */
142         if ( signals & window_signal )
143         {
144            WORD X_coord, Y_coord;
146            /* There may be more than one message, so keep processing messages until there are no more. */
147            while ( message = (struct IntuiMessage *)GetMsg(MyWindow->UserPort) )
148            {
149               /* Copy the necessary information from the message. */
150               msg_class = message->Class;
151               msg_code = message->Code;
152               X_coord = message->MouseX - MyWindow->BorderLeft;
153               Y_coord = message->MouseY - MyWindow->BorderTop;
155               /* Reply as soon as possible. */
156               ReplyMsg((struct Message *)message);
158               /* Take the proper action in response to the message. */
159               switch ( msg_class )
160               {
161                  case IDCMP_CLOSEWINDOW: /* User pressed the close window gadget. */
162                     end = TRUE;
163                     break;
164                  case IDCMP_MOUSEBUTTONS: /* The status of the mouse buttons has changed. */
165                     switch ( msg_code )
166                     {
167                        case SELECTDOWN: /* The left mouse button has been pressed. */
168                           left_mouse_button = TRUE;
169                           break;
170                        case SELECTUP: /* The left mouse button has been released. */
171                           left_mouse_button = FALSE;
172                           break;
173                     }
174                  case IDCMP_MOUSEMOVE: /* The position of the mouse has changed. */
175                     if ( left_mouse_button == TRUE )
176                     {
177                        WritePixel( MyWindow->RPort, X_coord, Y_coord );
178                     }
179                     break;
180                  default:
181                     break;
182               }
183            }
184         }
185      }
187      return;
188   }

Up until now our programs have been quite small and have consisted of only one function named main().  However, our programs are becoming ever larger and more complex.  Being able to divide the code into smaller sections would make it more manageable.  Functions allow us to do this, and also provide a great way to reuse code that may be needed in several places.  This approach to programming is called "structured programming" and is the philosophy with which C was developed.

(On a side note:  One of the popular offshoots to C, namely C++, was designed with a different philosophy called "object oriented programming".  This philosophy builds upon the benefits of structured programming to give the designers of extremely large programs an easier way of visualizing their large projects.  There is nothing special about programs written in C++ other than the approach used to write the code.  Anything that is created in C++ can also be written in C, although it may not look as pleasing to the eyes.  In fact, many early C++ compilers simply converted C++ programs into C prior to compiling.)

Lines 1 through 11 we have seen before.  They include the necessary definitions and prototypes for us to use the system functions.  Lines 14, 15, and 16 are new.  These lines are called function prototypes.  They notify the compiler that there will be three user-defined functions named program_init(), program_loop(), and clean_up().  Function prototypes are also used to tell the compiler what types of arguments can be passed to a function and what type of argument the function returns.  But before we continue let discuss what a function is in general terms.

In its most basic form a function is a method of processing information.  Some of you may recall learning about mathematical functions in school.  C functions are similar to mathematical functions in that both take in arguments and return a single result.  For instance, the function:

y = f(x)

is a simple mathematical function.  f() is the name of this generic function and both x and y are real numbers.  It doesn't really matter what the internals of the function are, all we need to know is that if we give the function an x it will return y.  Some of the more well-known functions in mathematics are the trigonometric functions like sine, cosine, and tangent.  These functions compute the ratio of different sides of a triangle.  For instance:

B = sine(A)

takes an angle A as an argument and returns the ratio B as the result.  Again, the actual workings of the sine function are unimportant to those who are only interested in the ratio.

In C, functions may have more than one argument, but only return one value as a result:

q = my_function(x, y, z);

This uses the numbers x, y, z and returns some value in q based on these.  Unlike mathematical functions, C function arguments and return values need not be only numbers.  The arguments can be any type defined by the user or one of C's internal types.  This is where function prototypes come in.  They let the compiler know that the program uses a particular function, what the types of arguments are expected, and what type of value (if any) the function returns.  The prototype on line 14:

int program_init(void);

tells the compiler that this program uses a function called program_init().  The
void between the parentheses means that this function doesn't take any arguments, and the int means that it will return an integer value when it is finished.  The other two prototypes on lines 15 and 16 take no arguments and return no values because void is used both in the parentheses and before the function name.  If prototypes are not used and a function is not defined prior to its use in the code, the compiler will usually assume that it returns an int and take the number and type of arguments that occur in the first use.  This is called an implicit prototype and will generally result in compiler warnings.

Now, technically all of this prototyping is not necessary, we could just move all of our function definitions from lines 97 through 184 and place them prior to the main() function on line 23.  If we do this the compiler will see that the functions are defined prior to their use in .  This approach will work well for small programs like we are writing now, but when we start writing large programs it is often impractical to write out all the function definitions before the main() function.  Using prototypes allows us to tell the compiler what a function looks like even if we have not written the internals of the function yet.  This way the compiler is not surprised when we use the function.  It's like saying to the compiler, "Hey, compiler, I'm going to be using some new functions in my program.  Here is what they look like, so don't be surprised when you see them in my code, ok?"  It's just a courtesy thing.  Prototypes will become very handy in later tutorials.

Lines 19 through 21 define the global pointers to the the libraries and window we will use.  Line 25 declares a variable used to hold the return value from program_init().  Line 27 actually calls program_init().  At this point program flow jumps to line 42--the first line of program_init().  If we look at the contents of this function we can see that it isn't really that different from close_window.c lines 22 through 66.  There are some items that have been added or changed, like line 45 which opens the graphics.library and the missing struct IntuiText, but the majority of the code is verbatim.  The theme here is that it is easy to split your programs into smaller sections.  Line 93 returns the result of the function: 0 if everything happened like it should, or less than zero if there was an error.  This value is then placed into the result variable when the program flow returns to line 27.

Line 29 is a test of the errors variable to determine if there have been any problems thus far.  Again, "!errors" is read "not errors" and causes the test to be true if errors is zero and false if errors is non-zero.  So if program_init() has no errors (i.e. returns zero), then program_loop() will be called.  Otherwise clean_up() is called to close any libraries or windows that program_init() did manage to open.

Now, what about the program_loop() function?  This is the main loop that checks for messages sent to our window and responds to each properly.  Note that the variable declarations on lines 123 through 129 are local to the program_loop() function.  This is an example of the proper use of local variables.  These variables are not available in other functions, and this reduces the possibility that another function could unexpectedly change their values and also frees us, the programmers, from having keep track of the names of all the variables in our entire program, we only need to remember the variables used in a particular function.  Line 124 is the only new local variable (window_signal) added to this example.  It will be used later to hold the signal bit for our window.

Line 132 loads window_signal with the value of MyWindow's signal bit.  The code "1L << MyWindow->UserPort->mp_SigBit" is used to compute the value of this signal bit.  Here's how it works:

The "<<" is called the left shift operator.  It is used to shift the actual binary digits in a number a specific number of places to the left.  It puts zeros in vacated bits.  (">>" is the right shift operator and works the same way, only it shifts bits to the right.)  In our case, we are shifting the bits in the value 1L.  Adding "L" is the way we ensure that a small value, such as 1, is extended to be 32 bits long (the size of a ULONG) instead of 16 or 8 bits.  Our example creates the 32-bit value of 1, which is:

00000000 00000000 00000000 00000001

MyWindow->UserPort->mp_SigBit holds the number of the particular bit used to represent the message signal for MyWindow.  So, if MyWindow->UserPort->mp_SigBit happens to be 26, then we will shift the 1 to the left 26 places:

00000010 00000000 00000000 00000000
      |------------ 26 -----------|

thus, the decimal value of window_signal would be 33,554,432.  That really doesn't matter here, because we're interested in the bit number much more than the value, but it may be useful to understand what's really going on.  Just remember that the signal bit for your window may be any number.

So lets look at the new items added to the main loop.  The first thing to note here is the call to Wait() on line 139.  This is a change from the SetSignal() used in close_window.c.  The reason for this change is that using SetSignal() in this context is actually extreamly inefficient. Wait() causes the program to pause and wait for a signal, but using SetSignal() allows the program to continue executing, even when there is not signal to process--this is called a busy loop because it is constantly running but not doing anything important.  A busy loop is bad because it steals processor time from other programs that could really use it.  Using Wait() stops our program until one or more of the specified signals are received. This frees up the processor for other tasks.  Once a signal is received, the program continues on as normal.  More than one signal can be passed to Wait() by separating  them by the OR ("|")operator:

signals = Wait( signal1 | signal2 );

The OR operator compares the bits in one binary number with the bits in another binary number.  If a bit is set in either number then it is set in the resulting number.

Here is an OR logic table (A | B = C):

OR Logic Table
0 0 0
0 1 1
1 0 1
1 1 1

As we can see, if either A or B is 1, then C is also 1.

When OR is applied to two binary numbers we get:

01000110 = A
11010111 = B
11010111 = C


01000110 | 11010111 = 11010111

Hopefully this is straight forward.

Once a signal is received, we must determine which signal it is.  As in line 142, we use the AND operator ("&") to compare the variable with the signal we are seeking:

if ( signals & window_signal )

ANDs the received signals with our window's signal bit.  This is done in much the same way as the OR operation, but it uses a different logic table:

AND Logic Table
0 0 0
0 1 0
1 0 0
1 1 1

When AND is applied to two binary numbers we get:

01000110 = A
11010111 = B
01000110 = C


01000110 & 11010111 = 01000110

So C is only true if both A and B are true, thus A "and" B.

So our test is true only if our window's signal bit is one of the signals that has been set, otherwise we go back to waiting for another signal.

Line 144 declares two local 16-bit WORD values that we will use to store the X and Y mouse coordinates.  The mouse coordinates are recorded with every message that is sent to the window and we will use them later to plot pixels in the window.

Line 146 through 156 are our normal message handling routines with the exception of line 152 and 153.  These two lines compute the window coordinates from the mouse coordinates in each message and assign these values to X_coord and Y_coord respectively.  The mouse coordinates are measured in pixels from the extreme upper left corner of the window.  MyWindow

We've declared our window with the WA_GimmeZeroZero tag.  This tag creates a separate drawing area inside the boundaries of the window's border that is offset by the size of the borders.  This requires more memory, but we don't need to worry about drawing over the window borders.  In the example window to the left the GimmeZeroZero drawing area is in gray.  This means that when we calculate the pixel coordinates from the mouse coordinates we must subtract the size of the left and upper borders from the X and Y mouse coordinate.  The border areas are in blue.  This ensures that the pixel directly underneath the mouse pointer is set to the new color.

Lines 159 through 182 actually handle the messages.  We've already seen how to handle the IDCMP_CLOSEWINDOW message, but there are two new messages that we handle in this program.  The first is the IDCMP_MOUSEBUTTONS message.  The second is the IDCMP_MOUSEMOVE message.

The IDCMP_MOUSEBUTTONS message is sent each time the mouse buttons change state.  That is, each time a mouse button is pressed or released.  This message contains an additional code found in msg_code.  This additional code indicates which mouse button is being referred to and if it has been depressed or released.  The Amiga OS supports mice with up to 3 buttons.  The left button, which we are most interested in for this example, is referred to as the "select" button.  Thus SELECTDOWN and SELECTUP codes refer to the left mouse button being either pressed (SELECTDOWN) or released (SELECTUP).  Similar codes exist for the middle button and the right "menu" button.  These codes can be found in the intuition/intuition.h header file.

Notice that when the select button is pressed left_mouse_button becomes TRUE and when it is released left_mouse_button becomes FALSE.  This is necessary because the SELECTDOWN and SELECTUP codes are only sent with the IDCMP_MOUSEBUTTONS message when the status of the button changes.  Remembering the sate of the button allows the user to hold the button down and draw, rather than trying to constantly click the button to plot a pixel.  You could try to remove these two assignments and see what happens.

The IDCMP_MOUSEMOVE message is handled next.  This message is sent every time the mouse pointer changes position on the screen.  Each time the mouse moves we check if the left mouse key is pressed (left_mouse_button == TRUE).  If it is pressed we call WritePixel() to plot the pixel at the current coordinates, otherwise we skip the plot and continue on.  The last two arguments are the adjusted X and Y coordinates we computed on lines 152 and 153.  The first argument of WritePixel() is a pointer to the struct RastPort of the window's drawing area. To the compiler, a raster port is really just a structure that contains information about a drawing surface.  Every area on the screen that can display and image is part of a raster port, much like a painter's canvas.  Every screen has its own raster port and every window also has its own. You can define your own raster ports using the graphics.library functions.  Take a look at the graphics/rastport.h header file to view the actual RastPort structure.  You can see that it contains pointers to bit maps (the actual memory areas where the image data is stored), it has information about fonts, line patters, drawing pens, etc.  All very useful stuff for anyone interested in graphics or user interface programming.

Now back to the code...

Notice that there is no break statement after line 173 and before the case
IDCMP_MOUSEMOVE on line 174.  The break statement causes the program flow to jump out of the current block, but since there is no break, the program flow "falls through" to the next case and executes the code for that case also.  This can be a handy feature of the switch statement.  In our example it allows us to use only one call to WritePixel() where two would be necessary otherwise.

And that's all there is to it!  Not too bad, right?  We've covered a lot of ground in the last couple of lessons and we're well on our way to mastering Amiga programming in C.  In the next lessons I'll explain some of the changes in the new Amiga OS 4 API and then we will move on to creating simple request dialog boxes and how to use them to get input from the user.

< Lesson 3: Close Window  |  Index  |  Lesson 5: OS4 Libraries >

If you find any errors or have any questions or comments please e-mail me.

Last updated on Saturday, February 11, 2006 21:08 -->