amiga-e/amigae33a/E_v3.3a/Docs/BeginnersGuide/Examples.guide

1190 lines
44 KiB
Plaintext

@database beginner.guide
@Master beginner
@Width 75
This is the AmigaGuide® file beginner.guide, produced by Makeinfo-1.55 from
the input file beginner.
@NODE "main" "Introduction to the Examples"
@Next "String Handling and I-O"
@Prev "OOE.guide/main"
@Toc "Contents.guide/main"
Introduction to the Examples
****************************
In this part we shall go through some slightly larger examples than
those in the previous parts. However, none of them are too big, so they
should still be easy to understand. The note-worthy parts of each example
are described, and you may even find the odd comment in the code. Large,
complicated programs benefit hugely from the odd well-placed and
descriptive comment. This fact can't be stressed enough.
All the examples will run on a standard Amiga, except for the one which
uses @{b }ReadArgs@{ub } (an AmigaDOS 2.0 function). It is really worth upgrading
your system to AmigaDOS 2.0 (or above) if you are still using previous
versions. The @{b }ReadArgs@{ub } example can only hint at the power and
friendliness of the newer system functions. If you are fortunate enough
to have an A4000 or an accelerated machine, then the timing example will
give better (i.e., quicker) results.
Supplied with this Guide should be a directory of sources of most of
the examples. Here's a complete catalogue:
@{b }simple.e@{ub }
The simple program from the introduction. See @{"A Simple Program" Link "Introduction.guide/A Simple Program" }.
@{b }while.e@{ub }
The slightly complicated @{b }WHILE@{ub } loop. See @{"WHILE loop" Link "Introduction.guide/WHILE loop" }.
@{b }address.e@{ub }
The program which prints the addresses of some variables. See
@{"Finding addresses (making pointers)" Link "Types.guide/Finding addresses (making pointers)" }.
@{b }static.e@{ub }
The static data problem. See @{"Static data" Link "Types.guide/Static data" }.
@{b }static2.e@{ub }
The first solution to the static data problem. See @{"Static data" Link "Types.guide/Static data" }.
@{b }except.e@{ub }
An exception handler example. See @{"Raising an Exception" Link "Exceptions.guide/Raising an Exception" }.
@{b }except2.e@{ub }
Another exception handler example. See @{"Raising an Exception" Link "Exceptions.guide/Raising an Exception" }.
@{b }static3.e@{ub }
The second solution to the static data problem, using @{b }NEW@{ub }. See
@{"List and typed list allocation" Link "Memory.guide/List and typed list allocation" }.
@{b }float.e@{ub }
The floating-point example program. See @{"Floating-Point Functions" Link "FloatingPoint.guide/Floating-Point Functions" }.
@{b }bintree.e@{ub }
The binary tree example. See @{"Binary Trees" Link "Recursion.guide/Binary Trees" }.
@{b }tree.e@{ub }
The @{b }tree@{ub } and @{b }integer_tree@{ub } classes, as a module. See @{"Inheritance in E" Link "OOE.guide/Inheritance in E" }.
@{b }tree-use.e@{ub }
A program to use the @{b }integer_tree@{ub } class. See @{"Inheritance in E" Link "OOE.guide/Inheritance in E" }.
@{b }set.e@{ub }
The simple, inefficient @{b }set@{ub } class, as a module. See @{"Data-Hiding in E" Link "OOE.guide/Data-Hiding in E" }.
@{b }set-use.e@{ub }
A program to use the @{b }set@{ub } class. See @{"Data-Hiding in E" Link "OOE.guide/Data-Hiding in E" }.
@{b }csv-estr.e@{ub }
The CSV reading program using E-strings. See @{"String Handling and I-O" Link "String Handling and I-O" }.
@{b }csv-norm.e@{ub }
The CSV reading program using normal strings. See
@{"String Handling and I-O" Link "String Handling and I-O" }.
@{b }csv-buff.e@{ub }
The CSV reading program using normal strings and a large buffer. See
@{"String Handling and I-O" Link "String Handling and I-O" }.
@{b }csv.e@{ub }
The CSV reading program using normal strings, a large buffer, and an
exception handler. See @{"String Handling and I-O" Link "String Handling and I-O" }.
@{b }timing.e@{ub }
The timing example. See @{"Timing Expressions" Link "Timing Expressions" }.
@{b }args.e@{ub }
The argument parsing example for any AmigaDOS. See @{"Any AmigaDOS" Link "Any AmigaDOS" }.
@{b }args20.e@{ub }
The argument parsing example for any AmigaDOS 2.0 and above. See
@{"AmigaDOS 2.0 (and above)" Link "AmigaDOS 2.0 (and above)" }.
@{b }gadgets.e@{ub }
The gadgets example. See @{"Gadgets" Link "Gadgets" }.
@{b }idcmp.e@{ub }
The IDCMP and gadgets example. See @{"IDCMP Messages" Link "IDCMP Messages" }.
@{b }graphics.e@{ub }
The graphics example. See @{"Graphics" Link "Graphics" }.
@{b }screens.e@{ub }
The screens example, without an exception handler. See @{"Screens" Link "Screens" }.
@{b }screens2.e@{ub }
The screens example again, but this time with an exception handler.
See @{"Screens" Link "Screens" }.
@{b }dragon.e@{ub }
The dragon curve recursion example. See @{"Recursion Example" Link "Recursion Example" }.
@ENDNODE
@NODE "String Handling and I-O" "String Handling and I-O"
@Next "Timing Expressions"
@Prev "main"
@Toc "Contents.guide/main"
String Handling and I/O
***********************
This chapter shows how to use normal strings and E-strings, and also
how to read data from a file. The programs use a number of the string
functions and make effective (but different) use of memory where possible.
The key points to understand are:
@{b }*@{ub } The difference between normal strings and E-strings.
@{b }*@{ub } The two methods of reading data from a file (line-by-line or all at
once).
@{b }*@{ub } The necessary allocation of memory for E-strings.
@{b }*@{ub } The unnecessary, but advisable, deallocation of the E-string memory
once it is no longer needed. The deallocation could be left to the
automatic deallocation at the end of the program, but that would
waste an increasing amount of memory whilst the program was running.
If the input data was large then memory could easily be exhausted.
@{b }*@{ub } The way in which sections of an E-string (or a normal string, for
that matter) can easily be turned into normal strings.
@{b }*@{ub } The way exception handlers can tidy up programs.
The problem to solve is reading of a CSV (comma separated variables)
file, which is a standard format file for databases and spreadsheets. The
format is very simple: each record is a line (i.e., terminated with a
line-feed) and each field in a record is separated by a comma. To make
this example a lot simpler, we will forbid a field to contain a comma
(normally this would require the field to be quoted). So, a typical input
file would look like this:
Field1,Field2,Field3
10,19,-3
fred,barney,wilma
,,last
first,,
In this example all records have three fields, as is well illustrated by
the first line (i.e., the first record). The last two records may seem a
bit strange, but they just show how fields can be blank. In the last
record all but the first field are blank, and in the previous record all
but the last are blank.
So now we know the format of the file to be read. To operate on a file
we must first open it using the @{b }Open@{ub } function (from the @{b }dos.library@{ub }), and
to read the lines from the file we will use the @{b }ReadStr@{ub } (built-in)
function. There will be four versions of a program to read a CSV file:
two of which read data line-by-line and two which read all the file at
once. Of the two which read line-by-line, one manipulates the read lines
as E-strings and the other uses normal strings. The use of normal strings
is arguably more advanced than the use of E-strings, since cunning tricks
are employed to make effective use of memory. However, the programs are
not meant to show that E-strings are better than normal strings (or vice
versa), rather they are meant to show how to use strings properly.
/* A suitably large size for the record buffer */
CONST BUFFERSIZE=512
PROC main()
DEF filehandle, status, buffer[BUFFERSIZE]:STRING, filename
filename:='datafile'
IF filehandle:=Open(filename, OLDFILE)
REPEAT
status:=ReadStr(filehandle, buffer)
/* This is the way to check ReadStr() actually read something */
IF buffer[] OR (status<>-1) THEN process_record(buffer)
UNTIL status=-1
/* If Open() succeeded then we must Close() the file */
Close(filehandle)
ELSE
WriteF('Error: Failed to open "\\s"\\n', filename)
ENDIF
ENDPROC
PROC process_record(line)
DEF i=1, start=0, end, len, s
/* Show the whole line being processed */
WriteF('Processing record: "\\s"\\n', line)
REPEAT
/* Find the index of a comma after the start index */
end:=InStr(line, ',', start)
/* Length is end index minus start index */
len:=(IF end<>-1 THEN end ELSE EstrLen(line))-start
IF len>0
/* Allocate an E-string of the correct length */
IF s:=String(len)
/* Copy the portion of the line to the E-string s */
MidStr(s, line, start, len)
/* At this point we could do something useful... */
WriteF('\\t\\d) "\\s"\\n', i, s)
/* We've finished with the E-string so deallocate it */
DisposeLink(s)
ELSE
/* It's a non-fatal error if the String() call fails */
WriteF('\\t\\d) Memory exhausted! (len=\\d)\\n', len)
ENDIF
ELSE
WriteF('\\t\\d) Empty Field\\n', i)
ENDIF
/* The new start is after the end we found */
start:=end+1
INC i
/* Once a comma is not found we've finished */
UNTIL end=-1
ENDPROC
There are a couple of points worth noting about this program:
@{b }*@{ub } A large E-string, @{b }buffer@{ub }, is used to hold each line before it is
processed. If a record exceeds the size of this E-string then
@{b }ReadStr@{ub } will only read a partial record, and the next @{b }ReadStr@{ub }
will read some more this record. However, the program considers each
call to @{b }ReadStr@{ub } to read a whole record, so it will get the records
slightly wrong in this case. This is a limitation of the program and
it should be documented so that users know to constrain themselves to
datafiles without long lines.
@{b }*@{ub } The file name is `hard-wired' to be @{b }datafile@{ub }. A more flexible
program would allow this to be passed as an argument (see
@{"Argument Parsing" Link "Argument Parsing" }).
@{b }*@{ub } @{b }ReadStr@{ub } may return -1 to indicate an error (usually when the end
of the file has been reached), but the E-string read so far may still
be valid. The check on the E-string and error value is the proper
way of deciding whether @{b }ReadStr@{ub } actually read anything from the file.
@{b }*@{ub } Look carefully at the manipulation of the string indexes @{b }start@{ub } and
@{b }end@{ub }, and the calculation of the length of a portion of a string.
@{b }*@{ub } @{b }MidStr@{ub } is used to copy a field from a record, so an E-string must
be used to hold the field.
@{b }*@{ub } The E-string @{b }s@{ub } is only valid between the successful allocation by
@{b }string@{ub } and the @{b }DisposeLink@{ub }. It would be incorrect to try to, for
instance, print it at any other point. On the other hand, a more
complicated program may want to store up all the data, and so it may
be inappropriate to deallocate the E-string at this point. In this
case, the pointer to the E-string could be stored and it might be
valid for the rest of the program.
@{b }*@{ub } The allocation using @{b }String@{ub } is very closely followed by deallocation
using @{b }DisposeLink@{ub }. This suggests that a single E-string could be
allocated and used repeatedly (like @{b }buffer@{ub } is), due to the simple
nature of this example.
To change this to use normal strings (in a very memory efficient way),
we need to alter only the @{b }process_record@{ub } procedure. Some note-worthy
differences are:
@{b }*@{ub } Small parts of the E-string @{b }buffer@{ub } are turned into normal strings by
terminating them with @{b }NIL@{ub } when necessary. This involves changing a
comma that is found.
@{b }*@{ub } No new memory is allocated, rather the @{b }buffer@{ub } memory is reused (as
described above). This is fine for this example, although if the
fields were needed after a record had been processed they would need
to be copied, since the contents of @{b }buffer@{ub } are changed by @{b }ReadStr@{ub }.
PROC process_record(line)
DEF i=1, start=0, end, s
/* Show the whole line being processed */
WriteF('Processing record: "\\s"\\n', line)
REPEAT
/* Find the index of a comma after the start index */
end:=InStr(line, ',', start)
/* If a comma was found then terminate with a NIL */
IF end<>-1 THEN line[end]:=NIL
/* Point to the start of the field */
s:=line+start
IF s[]
/* At this point we could do something useful... */
WriteF('\\t\\d) "\\s"\\n', i, s)
ELSE
WriteF('\\t\\d) Empty Field\\n', i)
ENDIF
/* The new start is after the end we found */
start:=end+1
INC i
/* Once a comma is not found we've finished */
UNTIL end=-1
ENDPROC
The next two versions of the program are basically the same: they both
read the whole file into one large, dynamically allocated buffer and then
process the data. The second of the two versions also uses exceptions to
make the program much more readable. The differences from the above
version which uses normal strings are:
@{b }*@{ub } The @{b }main@{ub } procedure calculates the length of the data in the file and
then uses @{b }New@{ub } to dynamically allocate some memory to hold it.
@{b }*@{ub } The read data is terminated with a @{b }NIL@{ub } so that it can safely be
treated as a (very long) normal string.
@{b }*@{ub } The @{b }process_buffer@{ub } procedure splits the read data up into lots of
normal strings, one for each line of data.
PROC main()
DEF buffer, filehandle, len, filename
filename:='datafile'
/* Get the length of data in the file */
IF 0<(len:=FileLength(filename))
/* Allocate just enough room for the data + a terminating NIL */
IF buffer:=New(len+1)
IF filehandle:=Open(filename, OLDFILE)
/* Read whole file, checking amount read */
IF len=Read(filehandle, buffer, len)
/* Terminate buffer with a NIL just in case... */
buffer[len]:=NIL
process_buffer(buffer, len)
ELSE
WriteF('Error: File reading error\\n')
ENDIF
/* If Open() succeeded then we must Close() the file */
Close(filehandle)
ELSE
WriteF('Error: Failed to open "\\s"\\n', filename)
ENDIF
/* Deallocate buffer (not really necessary in this example) */
Dispose(buffer)
ELSE
WriteF('Error: Insufficient memory to load file\\n')
ENDIF
ELSE
WriteF('Error: "\\s" is an empty file\\n', filename)
ENDIF
ENDPROC
/* buffer is like a normal string since it's NIL-terminated */
PROC process_buffer(buffer, len)
DEF start=0, end
REPEAT
/* Find the index of a linefeed after the start index */
end:=InStr(buffer, '\\n', start)
/* If a linefeed was found then terminate with a NIL */
IF end<>-1 THEN buffer[end]:=NIL
process_record(buffer+start)
start:=end+1
/* We've finished if at the end or no more linefeeds */
UNTIL (start>=len) OR (end=-1)
ENDPROC
PROC process_record(line)
DEF i=1, start=0, end, s
/* Show the whole line being processed */
WriteF('Processing record: "\\s"\\n', line)
REPEAT
/* Find the index of a comma after the start index */
end:=InStr(line, ',', start)
/* If a comma was found then terminate with a NIL */
IF end<>-1 THEN line[end]:=NIL
/* Point to the start of the field */
s:=line+start
IF s[]
/* At this point we could do something useful... */
WriteF('\\t\\d) "\\s"\\n', i, s)
ELSE
WriteF('\\t\\d) Empty Field\\n', i)
ENDIF
/* The new start is after the end we found */
start:=end+1
INC i
/* Once a comma is not found we've finished */
UNTIL end=-1
ENDPROC
The program is now quite messy, with many error cases in the @{b }main@{ub }
procedure. We can very simply change this by using an exception handler
and a few automatic exceptions.
/* Some constants for exceptions (ERR_NONE is zero: no error) */
ENUM ERR_NONE, ERR_LEN, ERR_NEW, ERR_OPEN, ERR_READ
/* Make some exceptions automatic */
RAISE ERR_LEN IF FileLength()<=0,
ERR_NEW IF New()=NIL,
ERR_OPEN IF Open()=NIL
PROC main() HANDLE
/* Note the careful initialisation of buffer and filehandle */
DEF buffer=NIL, filehandle=NIL, len, filename
filename:='datafile'
/* Get the length of data in the file */
len:=FileLength(filename)
/* Allocate just enough room for the data + a terminating NIL */
buffer:=New(len+1)
filehandle:=Open(filename, OLDFILE)
/* Read whole file, checking amount read */
IF len<>Read(filehandle, buffer, len) THEN Raise(ERR_READ)
/* Terminate buffer with a NIL just in case... */
buffer[len]:=NIL
process_buffer(buffer, len)
EXCEPT DO
/* Both of these are safe thanks to the initialisations */
IF buffer THEN Dispose(buffer)
IF filehandle THEN Close(filehandle)
/* Report error (if there was one) */
SELECT exception
CASE ERR_LEN; WriteF('Error: "\\s" is an empty file\\n', filename)
CASE ERR_NEW; WriteF('Error: Insufficient memory to load file\\n')
CASE ERR_OPEN; WriteF('Error: Failed to open "\\s"\\n', filename)
CASE ERR_READ; WriteF('Error: File reading error\\n')
ENDSELECT
ENDPROC
The code is now much clearer, and the majority of errors can be caught
automatically. Notice that the exception handler is called even if the
program succeeds (thanks to the @{b }DO@{ub } after the @{b }EXCEPT@{ub }). This is because
when the program terminates it needs to deallocate the resources it
allocated in every case (successful or otherwise), so the code is the same.
Conditional deallocation (of the buffer, for example) is made safe by an
appropriate initialisation.
If you feel like a small exercise, try to write a similar program but
this time using the @{b }tools/file@{ub } module which comes in the standard Amiga E
distribution. Of course, you'll first need to read the accompanying
documentation, but you should find that this module makes file interaction
very simple.
@ENDNODE
@NODE "Timing Expressions" "Timing Expressions"
@Next "Argument Parsing"
@Prev "String Handling and I-O"
@Toc "Contents.guide/main"
Timing Expressions
******************
You may recall the outline of a timing procedure in Part Two (see
@{"Evaluation" Link "MoreExpressions.guide/Evaluation" }). This chapter gives the complete version of this example.
The information missing from the outline was how to determine the system
time and use this to calculate the time taken by calls to @{b }Eval@{ub }. So the
things to notice about this example are:
@{b }*@{ub } Use of the Amiga system function @{b }DateStamp@{ub } (from the @{b }dos.library@{ub }).
(You really need the `Rom Kernel Reference Manuals' and the `AmigaDOS
Manual' to understand the system functions.)
@{b }*@{ub } Use of the module @{b }dos/dos@{ub } to include the definitions of the object
@{b }datestamp@{ub } and the constant @{b }TICKS_PER_SECOND@{ub }. (There are fifty ticks
per second.)
@{b }*@{ub } Use of the @{b }repeat@{ub } procedure to do @{b }Eval@{ub } a decent number of times for
each expression (so that some time is taken up by the calls!).
@{b }*@{ub } The timing of the evaluation of 0, to calculate the overhead of the
procedure calls and loop. This value is stored in the variable
@{b }offset@{ub } the first time the @{b }test@{ub } procedure is called. The
expression 0 should take a negligible amount of time, so the number
of ticks timed is actually the time taken by the procedure calls and
loop calculations. Subtracting this time from the other times gives
a fair view of how long the expressions take, relative to one another.
(Thanks to Wouter for this offset idea.)
@{b }*@{ub } Use of @{b }Forbid@{ub } and @{b }Permit@{ub } to turn off multi-tasking temporarily,
making the CPU calculate only the expressions (rather than dealing
with screen output, other programs, etc.).
@{b }*@{ub } Use of @{b }CtrlC@{ub } and @{b }CleanUp@{ub } to allow the user to stop the program if it
gets too boring...
@{b }*@{ub } Use of the option @{b }LARGE@{ub } (using @{b }OPT@{ub }) to produce an executable that
uses the large data and code model. This seems to help make the
timings less susceptible variations due to, for instance,
optimisations, and so better for comparison. See the `Reference
Manual' for more details.
Also supplied are some example outputs. The first was from an A1200
with 2MB Chip RAM and 4MB Fast RAM. The second was from an A500Plus with
2MB Chip RAM. Both used the constant @{b }LOTS_OF_TIMES@{ub } as 500,000, but you
might need to increase this number to compare, for instance, an A4000/040
to an A4000/030. However, 500,000 gives a pretty long wait for results on
the A500.
MODULE 'dos/dos'
CONST TICKS_PER_MINUTE=TICKS_PER_SECOND*60, LOTS_OF_TIMES=500000
DEF x, y, offset
PROC fred(n)
DEF i
i:=n+x
ENDPROC
/* Repeat evaluation of an expression */
PROC repeat(exp)
DEF i
FOR i:=0 TO LOTS_OF_TIMES
Eval(exp) /* Evaluate the expression */
ENDFOR
ENDPROC
/* Time an expression, and set-up offset if not done already */
PROC test(exp, message)
DEF t
IF offset=0 THEN offset:=time(`0) /* Calculate offset */
t:=time(exp)
WriteF('\\s:\\t\\d ticks\\n', message, t-offset)
ENDPROC
/* Time the repeated calls, and calculate number of ticks */
PROC time(x)
DEF ds1:datestamp, ds2:datestamp
Forbid()
DateStamp(ds1)
repeat(x)
DateStamp(ds2)
Permit()
IF CtrlC() THEN CleanUp(1)
ENDPROC ((ds2.minute-ds1.minute)*TICKS_PER_MINUTE)+ds2.tick-ds1.tick
PROC main()
x:=9999
y:=1717
test(`x+y, 'Addition')
test(`y-x, 'Subtraction')
test(`x*y, 'Multiplication')
test(`x/y, 'Division')
test(`x OR y, 'Bitwise OR')
test(`x AND y, 'Bitwise AND')
test(`x=y, 'Equality')
test(`x<y, 'Less than')
test(`x<=y, 'Less than or equal')
test(`y:=1, 'Assignment of 1')
test(`y:=x, 'Assignment of x')
test(`y++, 'Increment')
test(`IF FALSE THEN y ELSE x, 'IF FALSE')
test(`IF TRUE THEN y ELSE x, 'IF TRUE')
test(`IF x THEN y ELSE x, 'IF x')
test(`fred(2), 'fred(2)')
ENDPROC
Here's the output from the A1200:
Addition: 22 ticks
Subtraction: 22 ticks
Multiplication: 69 ticks
Division: 123 ticks
Bitwise OR: 33 ticks
Bitwise AND: 27 ticks
Equality: 44 ticks
Less than: 43 ticks
Less than or equal: 70 ticks
Assignment of 1: 9 ticks
Assignment of x: 38 ticks
Increment: 23 ticks
IF FALSE: 27 ticks
IF TRUE: 38 ticks
IF x: 44 ticks
fred(2): 121 ticks
Compare this to the output from the A500Plus:
Addition: 118 ticks
Subtraction: 117 ticks
Multiplication: 297 ticks
Division: 643 ticks
Bitwise OR: 118 ticks
Bitwise AND: 117 ticks
Equality: 164 ticks
Less than: 164 ticks
Less than or equal: 164 ticks
Assignment of 1: 60 ticks
Assignment of x: 102 ticks
Increment: 134 ticks
IF FALSE: 118 ticks
IF TRUE: 164 ticks
IF x: 193 ticks
fred(2): 523 ticks
Evidence, if it were needed, that the A1200 is roughly five times faster
than an A500, and that's not using the special 68020 CPU instructions!
@ENDNODE
@NODE "Argument Parsing" "Argument Parsing"
@Next "Gadgets IDCMP and Graphics"
@Prev "Timing Expressions"
@Toc "Contents.guide/main"
Argument Parsing
****************
There are two examples in this chapter. One is for any AmigaDOS and
the other is for AmigaDOS 2.0 and above. They both illustrate how to
parse the arguments to your program. If your program is started from the
Shell/CLI the arguments follow the command name on the command line, but
if it was started from Workbench (i.e., you double-clicked on an icon for
the program) then the arguments are those icons that were also selected at
that time (see your Workbench manual for more details).
@{" Any AmigaDOS " Link "Any AmigaDOS" }
@{" AmigaDOS 2.0 (and above) " Link "AmigaDOS 2.0 (and above)" }
@ENDNODE
@NODE "Any AmigaDOS" "Any AmigaDOS"
@Next "AmigaDOS 2.0 (and above)"
@Toc "Argument Parsing"
Any AmigaDOS
============
This first example works with any AmigaDOS. The first thing that is
done is the assignment of @{b }wbmessage@{ub } to a correctly typed pointer. At the
same time we can check to see if it is @{b }NIL@{ub } (i.e., whether the program was
started from Workbench or not). If it was not started from Workbench the
arguments in @{b }arg@{ub } are printed. Otherwise we need to use the fact that
@{b }wbmessage@{ub } is really a pointer to a @{b }wbstartup@{ub } object (defined in module
@{b }workbench/startup@{ub }), so we can get at the argument list. Then for each
argument in the list we need to check the lock supplied with the argument.
If it's a proper lock it will be a lock on the directory containing the
argument file. The name in the argument is just a filename, not a
complete path, so to read the file we need to change the current directory
to the lock directory. Once we've got a valid lock and we've changed
directory to there, we can find the length of the file (using @{b }FileLength@{ub })
and print it. If there was no lock or the file did not exist, the name of
the file and an appropriate error message is printed.
MODULE 'workbench/startup'
PROC main()
DEF startup:PTR TO wbstartup, args:PTR TO wbarg, i, oldlock, len
IF (startup:=wbmessage)=NIL
WriteF('Started from Shell/CLI\\n Arguments: "\\s"\\n', arg)
ELSE
WriteF('Started from Workbench\\n')
args:=startup.arglist
FOR i:=1 TO startup.numargs /* Loop through the arguments */
IF args[].lock=NIL
WriteF(' Argument \\d: "\\s" (no lock)\\n', i, args[].name)
ELSE
oldlock:=CurrentDir(args[].lock)
len:=FileLength(args[].name) /* Do something with file */
IF len=-1
WriteF(' Argument \\d: "\\s" (file does not exist)\\n',
i, args[].name)
ELSE
WriteF(' Argument \\d: "\\s", file length is \\d bytes\\n',
i, args[].name, len)
ENDIF
CurrentDir(oldlock) /* Important: restore current dir */
ENDIF
args++
ENDFOR
ENDIF
ENDPROC
When you run this program you'll notice a slight difference between @{b }arg@{ub }
and the Workbench message: @{b }arg@{ub } does not contain the program name, just the
arguments, whereas the first argument in the Workbench argument list is
the program. You can simply ignore the first Workbench argument in the
list if you want.
@ENDNODE
@NODE "AmigaDOS 2.0 (and above)" "AmigaDOS 2.0 (and above)"
@Prev "Any AmigaDOS"
@Toc "Argument Parsing"
AmigaDOS 2.0 (and above)
========================
This second program can be used as the Shell/CLI part of the previous
program to provide much better command line parsing. It can only be used
with AmigaDOS 2.0 and above (i.e., @{b }OSVERSION@{ub } which is 37 or more). The
template @{b }FILE/M@{ub } used with @{b }ReadArgs@{ub } gives command line parsing similar to
C's @{b }argv@{ub } array. The template can be much more interesting than this, but
for more details you need the `AmigaDOS Manual'.
OPT OSVERSION=37
PROC main()
DEF templ, rdargs, args=NIL:PTR TO LONG, i
IF wbmessage=NIL
WriteF('Started from Shell/CLI\\n')
templ:='FILE/M'
rdargs:=ReadArgs(templ,{args},NIL)
IF rdargs
IF args
i:=0
WHILE args[i] /* Loop through arguments */
WriteF(' Argument \\d: "\\s"\\n', i, args[i])
i++
ENDWHILE
ENDIF
FreeArgs(rdargs)
ENDIF
ENDIF
ENDPROC
As you can see the result of the @{b }ReadArgs@{ub } call with this template is an
array of filenames. The special quoting of filenames is dealt with
correctly (i.e., when you use " around a filename that contains spaces).
You need to do all this kind of work yourself if you use the @{b }arg@{ub } method.
@ENDNODE
@NODE "Gadgets IDCMP and Graphics" "Gadgets IDCMP and Graphics"
@Next "Recursion Example"
@Prev "Argument Parsing"
@Toc "Contents.guide/main"
Gadgets, IDCMP and Graphics
***************************
There are three examples in this chapter. The first shows how to open
a window and put some gadgets on it. The second shows how to decipher
Intuition messages that arrive via IDCMP. The third draws things with the
graphics functions.
@{" Gadgets " Link "Gadgets" }
@{" IDCMP Messages " Link "IDCMP Messages" }
@{" Graphics " Link "Graphics" }
@{" Screens " Link "Screens" }
@ENDNODE
@NODE "Gadgets" "Gadgets"
@Next "IDCMP Messages"
@Toc "Gadgets IDCMP and Graphics"
Gadgets
=======
The following program illustrates how to create a gadget list and use
it:
MODULE 'intuition/intuition'
CONST GADGETBUFSIZE = 4 * GADGETSIZE
PROC main()
DEF buf[GADGETBUFSIZE]:ARRAY, next, wptr
next:=Gadget(buf, NIL, 1, 0, 10, 30, 50, 'Hello')
next:=Gadget(next, buf, 2, 3, 70, 30, 50, 'World')
next:=Gadget(next, buf, 3, 1, 10, 50, 50, 'from')
next:=Gadget(next, buf, 4, 0, 70, 50, 70, 'gadgets')
wptr:=OpenW(20,50,200,100, 0, WFLG_ACTIVATE,
'Gadgets in a window',NIL,1,buf)
IF wptr /* Check to see we opened a window */
Delay(500) /* Wait a bit */
CloseW(wptr) /* Close the window */
ELSE
WriteF('Error -- could not open window!')
ENDIF
ENDPROC
Four gadgets are created using an appropriately sized array as the buffer.
These gadgets are passed to @{b }OpenW@{ub } (the last parameter). If the window
could be opened a small delay is used so that the window is visible before
the program closes it and terminates. @{b }Delay@{ub } is an Amiga system function
from the DOS library, and @{b }Delay(n)@{ub } waits n/50 seconds. Therefore, the
window stays up for 10 seconds, which is enough time to play with the
gadgets and see what the different types are. The next example will show
a better way of deciding when to terminate the program (using the standard
close gadget).
@ENDNODE
@NODE "IDCMP Messages" "IDCMP Messages"
@Next "Graphics"
@Prev "Gadgets"
@Toc "Gadgets IDCMP and Graphics"
IDCMP Messages
==============
This next program shows how to use @{b }WaitIMessage@{ub } with a gadget.
MODULE 'intuition/intuition'
CONST GADGETBUFSIZE = GADGETSIZE, OURGADGET = 1
PROC main()
DEF buf[GADGETBUFSIZE]:ARRAY, wptr, class, gad:PTR TO gadget
Gadget(buf, NIL, OURGADGET, 1, 10, 30, 100, 'Press Me')
wptr:=OpenW(20,50,200,100,
IDCMP_CLOSEWINDOW OR IDCMP_GADGETUP,
WFLG_CLOSEGADGET OR WFLG_ACTIVATE,
'Gadget message window',NIL,1,buf)
IF wptr /* Check to see we opened a window */
WHILE (class:=WaitIMessage(wptr))<>IDCMP_CLOSEWINDOW
gad:=MsgIaddr() /* Our gadget clicked? */
IF (class=IDCMP_GADGETUP) AND (gad.userdata=OURGADGET)
TextF(10,60,
IF gad.flags=0 THEN 'Gadget off ' ELSE 'Gadget on ')
ENDIF
ENDWHILE
CloseW(wptr) /* Close the window */
ELSE
WriteF('Error -- could not open window!')
ENDIF
ENDPROC
The gadget reports its state when you click on it, using the @{b }TextF@{ub }
function (see @{"Graphics functions" Link "BuiltIns.guide/Graphics functions" }). The only way to quit the program is
using the close gadget of the window. The @{b }gadget@{ub } object is defined in the
module @{b }intuition/intuition@{ub } and the @{b }iaddr@{ub } part of the IDCMP message is a
pointer to our gadget if the message was a gadget message. The @{b }userdata@{ub }
element of the gadget identifies the gadget that was clicked, and the
@{b }flags@{ub } element is zero if the boolean gadget is off (unselected) or
non-zero if the boolean gadget is on (selected).
@ENDNODE
@NODE "Graphics" "Graphics"
@Next "Screens"
@Prev "IDCMP Messages"
@Toc "Gadgets IDCMP and Graphics"
Graphics
========
The following program illustrates how to use the various graphics
functions.
MODULE 'intuition/intuition'
PROC main()
DEF wptr, i
wptr:=OpenW(20,50,200,100,IDCMP_CLOSEWINDOW,
WFLG_CLOSEGADGET OR WFLG_ACTIVATE,
'Graphics demo window',NIL,1,NIL)
IF wptr /* Check to see we opened a window */
Colour(1,3)
TextF(20,30,'Hello World')
SetTopaz(11)
TextF(20,60,'Hello World')
FOR i:=10 TO 150 STEP 8 /* Plot a few points */
Plot(i,40,2)
ENDFOR
Line(160,40,160,70,3)
Line(160,70,170,40,2)
Box(10,75,160,85,1)
WHILE WaitIMessage(wptr)<>IDCMP_CLOSEWINDOW
ENDWHILE
CloseW(wptr)
ELSE
WriteF('Error -- could not open window!\\n')
ENDIF
ENDPROC
First of all a small window is opened with a close gadget and activated
(so it is the selected window). Clicks on the close gadget will be
reported via IDCMP, and this is the only way to quit the program. The
graphics functions are used as follows:
@{b }*@{ub } @{b }Colour@{ub } is used to set the foreground colour to pen one and the
background colour to pen three. This will make the text nicely
highlighted.
@{b }*@{ub } Text is output in the standard font.
@{b }*@{ub } The font is set to Topaz 11.
@{b }*@{ub } More text is output (probably now in a different font).
@{b }*@{ub } The @{b }FOR@{ub } loop plots a dotted line in pen two.
@{b }*@{ub } A vertical line in pen three is drawn.
@{b }*@{ub } A diagonal line in pen two is drawn. This and the previous line
together produce a vee shape.
@{b }*@{ub } A filled box is drawn in pen one.
@ENDNODE
@NODE "Screens" "Screens"
@Prev "Graphics"
@Toc "Gadgets IDCMP and Graphics"
Screens
=======
This next example uses parts of the previous example, but also opens a
custom screen. Basically, it draws coloured lines and boxes in a big
window opened on a 16 colour, high resolution screen.
MODULE 'intuition/intuition', 'graphics/view'
PROC main()
DEF sptr=NIL, wptr=NIL, i
sptr:=OpenS(640,200,4,V_HIRES,'Screen demo')
IF sptr
wptr:=OpenW(0,20,640,180,IDCMP_CLOSEWINDOW,
WFLG_CLOSEGADGET OR WFLG_ACTIVATE,
'Graphics demo window',sptr,$F,NIL)
IF wptr
TextF(20,20,'Hello World')
FOR i:=0 TO 15 /* Draw a line and box in each colour */
Line(20,30,620,30+(7*i),i)
Box(10+(40*i),140,30+(40*i),170,1)
Box(11+(40*i),141,29+(40*i),169,i)
ENDFOR
WHILE WaitIMessage(wptr)<>IDCMP_CLOSEWINDOW
ENDWHILE
WriteF('Program finished successfully\\n')
ELSE
WriteF('Could not open window\\n')
ENDIF
ELSE
WriteF('Could not open screen\\n')
ENDIF
IF wptr THEN CloseW(wptr)
IF sptr THEN CloseS(sptr)
ENDPROC
As you can see, the error-checking @{b }IF@{ub } blocks can make the program hard to
read. Here's the same example written with an exception handler:
MODULE 'intuition/intuition', 'graphics/view'
ENUM WIN=1, SCRN
RAISE WIN IF OpenW()=NIL,
SCRN IF OpenS()=NIL
PROC main() HANDLE
DEF sptr=NIL, wptr=NIL, i
sptr:=OpenS(640,200,4,V_HIRES,'Screen demo')
wptr:=OpenW(0,20,640,180,IDCMP_CLOSEWINDOW,
WFLG_CLOSEGADGET OR WFLG_ACTIVATE,
'Graphics demo window',sptr,$F,NIL)
TextF(20,20,'Hello World')
FOR i:=0 TO 15 /* Draw a line and box in each colour */
Line(20,30,620,30+(7*i),i)
Box(10+(40*i),140,30+(40*i),170,1)
Box(11+(40*i),141,29+(40*i),169,i)
ENDFOR
WHILE WaitIMessage(wptr)<>IDCMP_CLOSEWINDOW
ENDWHILE
EXCEPT DO
IF wptr THEN CloseW(wptr)
IF sptr THEN CloseS(sptr)
SELECT exception
CASE 0
WriteF('Program finished successfully\\n')
CASE WIN
WriteF('Could not open window\\n')
CASE SCRN
WriteF('Could not open screen\\n')
ENDSELECT
ENDPROC
It's much easier to see what's going on here. The real part of the
program (the bit before the @{b }EXCEPT@{ub }) is no longer cluttered with error
checking, and it's easy to see what happens if an error occurs. Notice
that if the program successfully finishes it still has to close the screen
and window properly, so it's often sensible to use @{b }EXCEPT DO@{ub } to raise a
zero exception and deal with all the tidying up in the handler.
@ENDNODE
@NODE "Recursion Example" "Recursion Example"
@Next "Appendices.guide/main"
@Prev "Gadgets IDCMP and Graphics"
@Toc "Contents.guide/main"
Recursion Example
*****************
This next example uses a pair of mutually recursive procedures to draw
what is known as a dragon curve (a pretty, space-filling pattern).
MODULE 'intuition/intuition', 'graphics/view'
/* Screen size, use SIZEY=512 for a PAL screen */
CONST SIZEX=640, SIZEY=400
/* Exception values */
ENUM WIN=1, SCRN, STK, BRK
/* Directions (DIRECTIONS gives number of directions) */
ENUM NORTH, EAST, SOUTH, WEST, DIRECTIONS
RAISE WIN IF OpenW()=NIL,
SCRN IF OpenS()=NIL
/* Start off pointing WEST */
DEF state=WEST, x, y, t
/* Face left */
PROC left()
state:=Mod(state-1+DIRECTIONS, DIRECTIONS)
ENDPROC
/* Move right, changing the state */
PROC right()
state:=Mod(state+1, DIRECTIONS)
ENDPROC
/* Move in the direction we're facing */
PROC move()
SELECT state
CASE NORTH; draw(0,t)
CASE EAST; draw(t,0)
CASE SOUTH; draw(0,-t)
CASE WEST; draw(-t,0)
ENDSELECT
ENDPROC
/* Draw and move to specified relative position */
PROC draw(dx, dy)
/* Check the line will be drawn within the window bounds */
IF (x>=Abs(dx)) AND (x<=SIZEX-Abs(dx)) AND
(y>=Abs(dy)) AND (y<=SIZEY-10-Abs(dy))
Line(x, y, x+dx, y+dy, 2)
ENDIF
x:=x+dx
y:=y+dy
ENDPROC
PROC main() HANDLE
DEF sptr=NIL, wptr=NIL, i, m
/* Read arguments: [m [t [x [y]]]] */
/* so you can say: dragon 16 */
/* or: dragon 16 1 */
/* or: dragon 16 1 450 */
/* or: dragon 16 1 450 100 */
/* m is depth of dragon, t is length of lines */
/* (x,y) is the start position */
m:=Val(arg, {i})
t:=Val(arg:=arg+i, {i})
x:=Val(arg:=arg+i, {i})
y:=Val(arg:=arg+i, {i})
/* If m or t is zero use a more sensible default */
IF m=0 THEN m:=5
IF t=0 THEN t:=5
sptr:=OpenS(SIZEX,SIZEY,4,V_HIRES OR V_LACE,'Dragon Curve Screen')
wptr:=OpenW(0,10,SIZEX,SIZEY-10,
IDCMP_CLOSEWINDOW,WFLG_CLOSEGADGET,
'Dragon Curve Window',sptr,$F,NIL)
/* Draw the dragon curve */
dragon(m)
WHILE WaitIMessage(wptr)<>IDCMP_CLOSEWINDOW
ENDWHILE
EXCEPT DO
IF wptr THEN CloseW(wptr)
IF sptr THEN CloseS(sptr)
SELECT exception
CASE 0
WriteF('Program finished successfully\\n')
CASE WIN
WriteF('Could not open window\\n')
CASE SCRN
WriteF('Could not open screen\\n')
CASE STK
WriteF('Ran out of stack in recursion\\n')
CASE BRK
WriteF('User aborted\\n')
ENDSELECT
ENDPROC
/* Draw the dragon curve (with left) */
PROC dragon(m)
/* Check stack and ctrl-C before recursing */
IF FreeStack()<1000 THEN Raise(STK)
IF CtrlC() THEN Raise(BRK)
IF m>0
dragon(m-1)
left()
nogard(m-1)
ELSE
move()
ENDIF
ENDPROC
/* Draw the dragon curve (with right) */
PROC nogard(m)
IF m>0
dragon(m-1)
right()
nogard(m-1)
ELSE
move()
ENDIF
ENDPROC
If you write this to the file @{b }dragon.e@{ub } and compile it to the executable
@{b }dragon@{ub } then some good things to try are:
dragon 5 9 300 100
dragon 10 4 250 250
dragon 11 3 250 250
dragon 15 1 300 100
dragon 16 1 400 150
If you want to understand how the program works you need to study the
recursive parts. Here's an overview of the program, outlining the
important aspects:
@{b }*@{ub } The constants @{b }SIZEX@{ub } and @{b }SIZEY@{ub } are the width and height (respectively)
of the custom screen (and window). As the comment suggests, change
@{b }SIZEY@{ub } to 512 if you want a bigger screen and you have a PAL Amiga.
@{b }*@{ub } The @{b }state@{ub } variable holds the current direction (north, south, east or
west).
@{b }*@{ub } The @{b }left@{ub } and @{b }right@{ub } procedures turn the current direction to the left
and right (respectively) by using some modulo arithmetic trickery.
@{b }*@{ub } The @{b }move@{ub } procedure uses the @{b }draw@{ub } procedure to draw a line (of length
@{b }t@{ub }) in the current direction from the current point (stored in @{b }x@{ub }
and @{b }y@{ub }).
@{b }*@{ub } The @{b }draw@{ub } procedure draws a line relative to the current point, but
only if it fits within the boundaries of the window. The current
point is moved to the end of the line (even if it isn't drawn).
@{b }*@{ub } The @{b }main@{ub } procedure reads the command line arguments into the
variables @{b }m@{ub }, @{b }t@{ub }, @{b }x@{ub } and @{b }y@{ub }. The depth/size of the dragon is given by @{b }m@{ub }
(the first argument) and the length of each line making up the dragon
is given by @{b }t@{ub } (the second argument). The starting point is given by
@{b }x@{ub } and @{b }y@{ub } (the final two arguments). The defaults are five for @{b }m@{ub }
and @{b }t@{ub }, and zero for @{b }x@{ub } and @{b }y@{ub }.
@{b }*@{ub } The @{b }main@{ub } procedure also opens the screen and window, and sets the
dragon drawing.
@{b }*@{ub } The @{b }dragon@{ub } and @{b }nogard@{ub } procedures are very similar, and these are
responsible for creating the dragon curve by calling the @{b }left@{ub }, @{b }right@{ub }
and @{b }move@{ub } procedures.
@{b }*@{ub } The @{b }dragon@{ub } procedure contains a couple of checks to see if the user
has pressed Control-C or if the program has run out of stack space,
raising an appropriate exception if necessary. These exceptions are
handled by the @{b }main@{ub } procedure.
Notice the use of @{b }Val@{ub } and the exception handling. Also, the important
base case of the recursion is when @{b }m@{ub } reaches zero (or becomes negative,
but that shouldn't happen). If you start off a big dragon and want to
stop it you can press Control-C and the program tidies up nicely. If it
has finished drawing you simply click the close gadget on the window.
@ENDNODE