![]() | Lesson: MySQL Screensaver |
By Christopher Atkinson Code ported from BC5.02 for MSVC by Brian Tegart List of features that will be handled in the scope of this tutorial:
The objective of this tutorial is to create a screensaver that, when executed, tries to access a database across the network at a specific IP to retrieve a poem from it in a modified text format that will then be displayed on the screen. If the server is offline or the database server is down, a built-in poem is used (I chose this to be "Funeral Blues" by W.H. Auden. Some of you may recognize it from the movie Three Weddings and a Funeral). The text is then displayed on the screen in an enjoyable fashion (lots of code for you to go through on your own :)). To start off with this lengthy explanation, a few notes on this project as a whole are in order. We will be building a brand new base code (based on NeHe’s base code) that has one very different, but useful orientation: an Object Oriented approach. There’s quite a bit of code we will not go through in the tutorial itself, that will be commented in the source files – this will force you to read and try and understand it. All the essentials (the screensaver framework and MySQL stuff) will be explained in more detail, though. Why OO? Otherwise this screensaver would be very difficult to comprehend – especially if you’re interested in only one of the aspects (such as the screensaver framework, but not the wrapper classes). So, with that all aside, let’s get started! The SQL language One of the objectives of this tutorial is to demonstrate the use of libMySQL.dll (no, we will not be doing any connecting to a database on our own – instead we’ll let MySQL handle all of the database business). At this point, anyone with an understanding of how a database works and an understanding of the SQL language can skip this section. What exactly is SQL? The acronym stands for Standard Query Language, which is the most widely used database language in the world. It is a very high level language and will therefore seem a whole lot like normal speech. We will be concentrating on the MySQL database, which is used as the underlying database engine by almost any major wrapper (such as MS Access). There are many versions of the SQL language distributed by different vendors (Oracle, FoxPro, MS, etc), so finding a common way to communicate with them is almost impossible. We will be using, as mentioned above, the (freeware) MySQL library available at [1]. Databases traditionally contain two base types of entities: tables and queries. Tables are generally in the form:
Table 1. An example of a table in a database There are three key pieces of information in the above table (the types in capital letters are MySQL- specific and will be used in the next section – for now think of them as the equivalents in the types’ table below):
There are three primary types of queries that you can use: the insert, select and delete queries. There are tons of additional keywords that allow for very intricate and complex queries (that can then take days to execute) for you to use. We will only be dealing with simple queries in this tutorial. We will now look at the use of the three above query types plus an example of how to start using a completely empty MySQL database with only command line support. Let’s suppose you have just got yourself a completely blank database (that contains no tables, no nothing), and you need to set up a viable database necessary for this tutorial. Basically, you need to create the above table (Table 1). You do that using the CREATE TABLE keywords (we will be using uppercase notation to better identify SQL keywords from here on): CREATE TABLE |
CREATE TABLE `Poems ` ( `ID` BIGINT NOT NULL AUTO_INCREMENT, `Poem` LONGTEXT NOT NULL , `Author` MEDIUMTEXT NOT NULL , `Name` MEDIUMTEXT NOT NULL , `Notes` LONGTEXT NOT NULL , PRIMARY KEY ( `ID` ) , FULLTEXT ( `Poem` , `Author` , `Name` , `Notes` ) );
|
See the next section for an explanation on how to do this automatically in the MySQL wrapper kit. At
this point we will only have a look at the SQL syntax. We’re using the MySQL syntax, so the picture is
slightly different from what we looked at above. The name of the table we’re creating is "Poems" and it
contains five fields. None of the fields can be left empty by definition (note the NOT NULL part).
However, this isn’t the real case here since four of the fields are defined to be FULLTEXT fields and
an empty string is a completely valid input: "". We have defined ID to be the primary key and set it to
automatically increment every time a record is added. It is very important that we did not mess with
this value later on when inserting stuff!
Once we have created the table, it is initially completely empty. Note that in the MySQL kit used in this tutorial, you will also have to explicitly create a database (to do that we simply pass "CREATE DATABASE MyDatabaseName;" to the database engine). We now want to add something to the table we just created. This is done using the INSERT keyword: INSERT |
INSERT INTO `Poems ` ( `ID` , `Poem` , `Author` , `Name` , `Notes` ) VALUES ( '', 'Stop all the clocks, cut off the telephone, ...', 'W. H. Auden', 'Funeral Blues', '' );
|
What happens here is that only three of the four values are inserted into the database – the primary key
is left up to the database engine to be defined and no Notes are added. Study the syntax – the SQL
language has been made as naturally readable as possible so it shouldn’t be too much of a problem.
Now, let’s suppose we have inserted some 10 or 15 records into the table and are now keen on retrieving them. In particular, we want to retrieve the poem Funeral Blues by W. H. Auden. Here’s how we do that: SELECT First case: we want to see the entire record: |
SELECT * FROM `Poems` WHERE `Author` LIKE 'W. H. Auden' AND `Name` LIKE ‘Funeral Blues’;
|
Simple isn’t it? The asterisk denotes that all of the data in the specified record will be returned. If we
want to retrieve all of the data in the table, we could just omit the WHERE clause.
Second case: we only want to see the poem itself, no author and other info: |
SELECT `Poem` FROM `Poems` WHERE `Author` LIKE 'W. H. Auden' AND `Name` LIKE ‘Funeral Blues’;
|
Select may be simple, but it is very powerful. In conjunction with INNER JOIN, UNION and other
keywords, one can create mind boggling queries. However, this is well beyond the scope of this tutorial.
Now, let’s suppose we don’t want any of W. H. Auden’s poems to exist in your database because we find them to be too morose. This is where we use the DELETE keyword: DELETE |
DELETE FROM `Poems` WHERE `Author` LIKE 'W. H. Auden';
|
Et voila! We have created our own database, added a table to it, added a record to that table and deleted
it. We could now use DROP `Poems`; to get rid of the table as well. Now let’s see how to do all this
semi-automatically in a MySQL database.
If the above syntax looks intimidating, don’t fret – in the next section we will learn how to automate this process! The MySQL database kit Retrieve the kit at [1] under the downloads section. We’ll leave setting it up to you – there is a very comprehensive online manual for the product. MySQL is of course under GPL, meaning it is freely available to use for everything. It can be used for both non-commercial and commercial purposes. From my personal experience, it doesn’t take a rocket scientist to get it working – in fact it’s rather straightforward. Nevertheless, let’s go over the procedures described in the previous section. Once having set up the engine and a new database for yourself, click the Structure tab and from there enter the name for the new table and the number of fields you want to appear in it (follow the instructions on the page). Once you click Go you will be prompted to enter the fields’ descriptions. This is where you will meet the BIGINT and other MySQL specific data types. Refer to [1] for a more comprehensive explanation on what each of them means. For the time being, however, simply make the fields as described in Table 1. Create all of the fields except for the primary key (set the appropriate radio button) as FULLTEXT. Once you’ve done that, you can click ‘Insert’ in the ‘Actions’ section next to the table name under the Structure tab. You can run select queries by entering a more comprehensive menu by clicking ‘Settings’ just next the ‘Add’ button, and from there choosing the ‘Browse’ tab. The rest I will leave up to you to figure out – it’s time to move on to the real topic of this tutorial. VERY IMPORTANT: please do NOT e-mail me questions about setting up the MySQL database engine – this has got nothing to do with the tutorial – it is simply an unfortunate necessity that needs to be tackled. The MySQL homepage will provide you with all the information in the world! Getting to the topic: untangling the myth The myth is about screensavers. Rumor has it a screensavers are something completely different from normal executables. And in a way they are. Then again, they bear no difference at all. In the following section we will try and create a brief overview of what we need to know about a screensaver. First off, let’s imagine how a normal Win32 application functions: we have WinMain() which is used as the entry point of the program and is responsible for the initialization and normal termination of the application. It’s the same for a screensaver. In a video-oriented application, such as a game, a window is then created and a loop run that keeps calling some draw function. It’s the same for a screensaver! When creating a project in the IDE, we specify a Win32 application as the target for a normal program. We do the same for a screensaver, the only difference being the fact that we actually specify the native screensaver extension .scr as the wildcard instead of the traditional .exe. So far not too many differences are there? Here’s the catch, though: a normal application is run only whenever the user explicitly runs the .exe or it is run by some scheduler or some batch file. While these conditions also hold true for a screensaver, these are not the normal means of execution for our case. There are four distinct modes a screensaver can be run in. These are: normal, preview, configuration and password modes. Next comes a little explanation on each of them, how and when they’re executed:
The command line "But how do I know in which mode to run when the saver is executed?", you might ask. The answer is simple – again, based on command line flags. We will now start going through some of the essential code from the top. The first and most important part is the command line parser. It can look a little messy, but let’s talk our way through it. So, the first thing we do in WinMain(), is parse the command line to learn what we must do next: |
// skip over the path char* p = strtok(GetCommandLine(), " "); p = strtok(NULL, " ");
|
I decided to refrain from the traditional line-by-line commenting since there’s a lot of code to cover and
things might become fuzzy. Instead we will explain sections such as this by dissecting them separately.
First off, we’re using a very useful standard library function called strtok() here. The name itself stands for "string tokenizer" and it allows us to split a string into smaller sections while changing the delimiter fter each split. We’re not using this feature here since our delimiter is a whitespace in all cases. Here’s how strtok() operates: the first time we call it we pass it the string that we’re about to tokenize. We also pass it the first delimiter at which we want the function to give us our first portion of the string. The next time we call strtok() and we want to continue tokenizing the same string, we are passing it NULL as the first argument. Since strtok() effectively ruins the pointer we give to it there is no way we could call GetCommandLine() again after this piece of code and get the correct result!! Then again, we won’t need to call it again, so we can do whatever we want with it. If you don’t understand why the string is ruined, start approaching the problem from the point of view of memory addresses (pointers) instead of text strings. |
if(p != NULL)
{
// there's a backslash or a dash here - skip over it (and any other such stuff)
while(!isalpha(*p))
p++;
| This does nothing more than skips over all non-alphabetic characters in the last token returned by strtok(). If you’re not familiar with pointer arithmetic then this can seem slightly awkward – actually it’s very useful: we keep incrementing the pointer by blocks of size byte and after each increment check if the current byte is alphabetical or not. |
//check for the argument flag
switch(*p)
{
case 's': case 'S':
ScrMode =ModeNormal;
goto End;
case 'p': case 'P':
case 'l': case 'L':
ScrMode = ModePreview;
goto GetHWND;
case 'c': case 'C':
ScrMode =ModeCfg;
goto GetHWND;
case 'a': case 'A':
ScrMode =ModePwd;
goto GetHWND;
}
This is the core of the whole parser – here we decide which mode we have to run our saver in. These
flags are predefined and passed to the screensaver by Windows. ModeNormal means that the screensaver
should switch to fullscreen and conduct its honorary business of cheering us up. The rest should be
clear by now. For three of those modes we potentially have to acquire some kind of a parent window to
build on. To cut down code size, we use the goto statement – quite useful in this case. Here’s a listing of
the command line flags as given at [3]:
|
GetHWND:
// skip over any non-digits
while(!isdigit(*p))
p++;
//now acquire the window into which we're going to be drawing later on
Handle = (p == NULL) ? GetForegroundWindow() : (HWND)atol(p); // <--
End:
| We jump to the label GetHWND if we’re expecting a window handle and to End, if not. So, what does the arrowed line do? If we’re given a valid window handle, we convert it to a 32-bit value and use it, otherwise we acquire the foremost window via GetForegroundWindow(). For the sake of the size of this tutorial, we will not be implementing the "scrprev" preview functionality. "scrprev" allows the screensaver to automatically acquire a Preview Window. This is a built-in functionality of Windows to aid us in the debugging process. All of this can be done by running the screensaver in the designated preview window on the Display Options dialog. If you feel like taking a stab at it yourself, refer to [2] – it’s not that difficult, but it hardly provides any additional functionality and most distributable screensavers have it either removed or disabled. |
} else ScrMode = smConfig;
|
If the screensaver was run without arguments, we presume configuration mode.
Initializing in the proper mode Now, let’s go over some theory again for a change. Namely, the requirements posed on a screensaver are quite a bit different than those that apply to most normal programs. While a screensaver that crashes normally doesn’t do much harm, it can be very annoying since these programs are run automatically by the OS. Therefore it is important to think through as to what the user should be notified of, if at all. A screensaver should, in my opinion at least, never pop up an error message or crash (since this can actually take the entire OS down with it – in theory at least). Since we set our aims on "no pop-ups", we must be able to either handle everything internally or write very clean code. The latter is often not an option, but we’ll try :). Another factor we have to keep our eye on is the number of files that is necessary to run the screensaver – that’s why we won’t be using any textures and all of the settings will be stored in the system registry rather than a configuration file. This means that we have to store the default settings internally and update the registry when the screensaver is run for the first time. |
ReadRegistry();
|
We try to read all of the settings from the registry – see ReadRegistry() below for a comprehensive walkthrough.
Next we call a weird function that I believe you might not be familiar with: |
InitCommonControls();
|
What it does is self-explanatory, but why we call it is not at all that clear. Namely, if we run the
screensaver in configuration mode, we can see a slider bar control. This is a common Windows control,
but isn’t readily available for an application to use before explicitly evoked. That’s what InitCommonControls()
does. It actually enables us to use a lot of other neat controls as well, such as tab controls and
treeviews, etc, but we won’t be dealing with these.
Next comes the initialization of the screensaver itself. Based on the choices made earlier, we now run the appropriate portion of the program. Note that for a lengthier explanation on the TGLWindow and other classes see the end of this tutorial. |
switch(Mode)
{
// if we're in preview mode, we want to create the saver in a
// smaller window (for which we have stored the handle in Handle).
case ModePreview:
RECT rc;
// Since we don't know the precise size of the preview window,
// we must retrieve it
GetWindowRect(Handle, &rc);
// see the TGLWindow class for specs on this function call
Window = new TGLWindow(Handle, NULL, ScreenSaverWindowProc,
"ScreenSaverPreview","SaverPreview", 0, 0, rc.right - rc.left,
rc.bottom - rc.top, 32, false);
break;
// in normal mode we do the same, but also switch to fullscreen.
// GetSystemMetrics() with the appropriate arguments retrieves
// the current desktop resolution
case ModeNormal:
Window = new TGLWindow(Handle, NULL, ScreenSaverWindowProc,
"Poetic Saver", "SaverMain", 0, 0, GetSystemMetrics(SM_CXSCREEN),
GetSystemMetrics(SM_CYSCREEN), 32, true);
break;
// if we must initialize in the Configuration mode, we simply
// create the dialog box for which we have given the ID
// DLG_SCRNSAVECONFIGURE, stored in the resource file. DialogBox()
// is a standar WinAPI call - check it out on MSDN
case ModeCfg:
ConfigDlg = DialogBox(hInstance, MAKEINTRESOURCE(DLG_SCRNSAVECOFIGURE), Handle, ScreenSaverConfigureDialog); // <-
break;
// we don't handle password stuff! For NT Windowses, the OS does
// it for us. For non-NT OS'es, the password must be disabled.
case ModePwd:
return 0;
break;
}
|
Pretty much everything is explained by the comments, except for one thing – the line denoted by the
arrow comment. So what’s up with that? What is the last argument we’re passing to DialogBox()? If you’re not
familiar with system callback functions, here’s the place to get you up to speed. So, what are these
callback functions? These are specific function prototypes provided by the OS so that it can keep track
of stuff. Ideally, every window has a callback function – most of the time they’re hidden from us,
though (such as in the case of a button or an edit box). However, for each custom window, we must
handle it. You’ve actually done that repeatedly when creating the main window for your favorite
application (not necessarily OpenGL). In fact, we have another one in this very tutorial! Can’t see it,
can you? That’s because we’ve hidden it in the TGLWindow class as a static member function. "But if it is
hidden in the window class, then how can I access it for each individual window", you might ask. In
this tutorial you can’t! You’d need to pass the window class a pointer to a public function that is then
called by the window class’ callback function. Confusing, isn’t it. Relax – you don’t have to get this for
the sake of this tutorial :). This is actually what subclassing is all about (we will look at it briefly near
the end) – things are actually slightly more complicated than they seem at the first glance. And,
surprise-surprise! There’s yet another system callback function here! It’s called ScreenSaverWindowProc()
and will also be explained later on.
So with all of that aside, we can think of the ScreenSaverConfigureDialog callback as a function that is called by Windows every time our configuration dialog needs to be sent a message. We will look at it more closely later on. For now, let’s move on with WinMain(). |
// if we're not in configuration or password mode
if(Mode != ModeCfg && Mode != ModePwd)
{
// tell our main window that we're doing our drawing stuff in the
// DrawGLScene function
Window->BindDrawFunc(DrawGLScene);
// capture the rendering context
Window->Capture();
| This place should be the first one to actually need explaining. The above two function calls can be very easily understood if you think of two completely different things: how GLUT works and multi-window support. Let’s elaborate: if you haven’t used GLUT then this might be new to you, otherwise you should be familiar with the concept of passing pointers to functions to some class/API. Namely, drawing a frame in a window requires three steps: we must make the rendering context bound to the window we currently want to draw in (wglMakeCurrent()), we can then draw into that rendering context, and finally we need to call SwapBuffers() to paste the image onto the screen. Now, imagine having a big window with a lot of smaller child windows that all use OpenGL to draw their contents and we want to loop through them, drawing them one by one. Using a class-based approach, in this case, is godly and lifesaving. Based on the above, we need to call wglMakeCurrent() before drawing into each of these windows, then call some draw function and then SwapBuffers(). Calling these functions requires a small list of parameters, though – parameters that are safely stored within the TGLWindow class. So, what we’ve done here is that we’ve made wglMakeCurrent() a parameterless member function of the window class called Capture() and we’re letting TGLWindow also manage the SwapBuffers() call. Now, by calling Window->Draw(), we’re internally calling the function we passed a pointer for to the TGLWindow class. After we return from DrawGLScene(), TGLWindow also calls SwapBuffers(). Still confused? Be not alarmed, function pointers require a little bit of getting used to at first. There may be one thing that’s bothering you at this point, though. Why not let TGLWindow also handle the call to wglMakeCurrent() internally? This wouldn’t be smart because we might want to do stuff between drawing and capturing the rendering context. If you feel like calling SwapBuffers() automatically is also a violation of this freedom, remove it from the window class – I like it there, though :). |
// create a timer and reset it Timer = new TTimer(); Timer->Push();
| The timer class has a couple of new features that will be used throughout this project and one very important peculiarity. The features are Push() and Pop() member functions. Push() sets the instance it is called as the zero offset for the counter and Pop() returns the offset since the last call to Push(). All numbers are in milliseconds. The peculiarity of the timer class is scope. Namely, the timer only returns a viable figure when it is called within the same module (file) as it is created in. This means that initializing a TTimer class in main.cpp and calling Timer->Pop() in another.cpp will yield wrong results. Why? That’s because the entire timer class is defined in a header. This saves us from including another .cpp file, but forces us to take imaginative action to synchronize time between all of the modules. This "imaginative action" manifests itself in the form of the function GetAppTime(). |
// we're using a custom font BuildFont();
| If you need to refresh yourself on font creation, check out NeHe’s tutorial 13. |
// try to retrieve a poem from the online database char* Text = RetrieveText(); // if something went wrong and we don't have a poem, use the built-in one if(Text == NULL) Text = Poem; // set it read to to be drawn - build a TSCText class to hold the entire // poem text Txt = new TSCText(Window, Font, Timer); // the poem text is formatted unsuitably for direct displaying, so parse it Txt->Parse(Text); // we want to use two different effects, so register them Txt->AddKeyframe(SwoopIn); Txt->AddKeyframe(SwoopOut); // run the text! Txt->Start(); }
|
Everything related to the TSCText and the underlying TSCString and TSCCharacter classes is not a part of this
tutorial and will be left for you to explore on your own. They’re all commented and if you need further
explanation or have some suggestions, e-mail me. VERY IMPORTANT: if you want to exclude all of
the overhead these classes bring to the saver code, just remove all reference to them and edit
DrawGLScene() to draw your favorite spinning cube! It’s that simple.
The main loop Now for the main program loop: |
// you should be familiar with first part of this loop
bool done = false;
while(!done)
{
if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
if(msg.message == WM_QUIT)
done = TRUE;
else
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
else
{
// this is new. this means that, if we're not in config or password mode,
// we should draw in the main window
if(Mode != ModeCfg && Mode != ModePwd)
Window->Draw();
// give the cpu some slack
Sleep(5);
}
| This is where we’re executing all of the might of object-oriented programming. Our call to Window->Draw() unleashes an avalanche of calls tightly packed into just a few lines. Even though you might say everything up to this point can be done procedurally (without OO), object-orientedness still has its clear advantages, one them being encapsulation. Can you see any arrays for anything floating around in main.cpp? I can’t :). That’s because we’ve hidden them all. Decrypting these two calls, Window->Draw() calls DrawGLScene() (can you see how?) and Sleep(5) causes the application to give the CPU five milliseconds of rest after each frame. Now why would we want to do that? A personal example: if I leave my computer unattended and the screensaver kicks in, I want other processes running in the background (such as Prime95, or for some of you, some Cure For Cancer or SETI application) to get more CPU time. The screensaver doesn’t need 99% of CPU usage - at this point there isn’t much wisdom in creating a low priority thread for it either. So, let’s simply skip a couple of milliseconds each frame. Now what are the drawbacks in this case? One very major factor is frame rate: for sleep time = 5 ms, maximum theoretical frame rate cannot be higher than 1000 / 5 = 200 fps. Period. Increase this value further to give up more CPU time. |
} // we're exiting - let's destroy all the evidence! if(ConfigDlg) EndDialog(Handle, ConfigDlg);
|
This is standard procedure for dialog boxes – EndDialog() is a native WinAPI call. This also concludes our
WinMain() function.
RetrieveText() – connecting to the MySQL database We will be moving on with some of the functions called from WinMain() that were not given much consideration. First comes RetrieveText() – this will force us to deal with the only practical section of SQL in the entire screensaver! It’s simple, I promise :). The source code may be slightly under commented, so we’ll try to make up for it here. |
#include "include/mysql.h"
| Let’s be sure to include the appropriate header provided with the MySQL kit described at the beginning of this tutorial. Also keep in mind that we have to include the appropriate library, libmysql.lib! |
// RetrieveText() connects to a remote database and tries to retrieve a
// poem text. If it fails NULL is returned.
char* RetrieveText()
{
char* Text = NULL;
// create an SQL object
MYSQL* MySQLObject;
// create a database record object
MYSQL_ROW Row;
| See [4] for the root directory of the online documentation. The MYSQL object is the fundamental component of the database access. We use MYSQL_ROW to retrieve one record from the query. |
// try to initialize the SQL object
if(!(MySQLObject = mysql_init((MYSQL*)0)))
{
WinError("Cannot initialize MySQL client");
return NULL;
}
| This should be pretty much self-explanatory. Just in case: we’re initializing the MySQLObject object (more precisely, the MySQL library does that). WinError() is a macro defined as a shortcut for the common MessageBox() WinAPI function call. See main.h. |
// try to connect to a remote database
if(!mysql_real_connect(MySQLObject, ServerIP, Username, Password, Databasename, ServerPort, NULL, 0))
{
delete MySQLObject;
WinError("Cannot connect to database");
return NULL;
}
| This is where connecting to the database takes place. The parameters for mysql_real_connect() are taken from the configuration info. Edit them in the configuration mode and allow the saver to store the parameters in Windows Registry if you want to connect to your own database. |
int Length;
// submit the query
if(!mysql_query(MySQLObject, QueryText))
{
| This executes the query. If it returns NULL, we’re in the clear and can start going through the returned results. |
// acquire the record from the returned query results Row = mysql_fetch_row(mysql_store_result(MySQLObject));
| We only need one record from MySQLObject (even if it contains more), so we only fetch one. The result is stored in Row. |
// get the length of the record Length = strlen(*Row);
| MySQL stores the returned results in string format. A remark is in order here: the MYSQL_ROW variable type is essentially a char**, or a pointer to pointer to char which means that it holds a list of strings in it. We have to keep this in mind when creating the query. For instance, if we were to retrieve more than one field from the database and we wanted to use all of them, we could only access the first field as *Row (a dereferenced double pointer becomes a simple pointer to a char, eg a string). If we need to access any succeeding string in the string list, we could just point to it like this: *(Row + 1) or do it the traditional way: Row[n], where n is the number of the string we wish to access. |
// get the length and allocate memory for the text
if(Length > 0)
Text = new char[Length];
// copy the entire record into Text
for(unsigned i = 0, j = 0; i < Length; i++)
{
// if there's a field change in the record, skip the newline
if((unsigned char)*(*Row + i + 1) == '\n')
continue;
else
// otherwise just copy the character
Text[j++] = (char)*(*Row + i);
}
| Now, why do we need this loop? In fact, we don’t if we only want to make the program work. We added it to the saver code to add one very important thing to the final product: flexibility. Namely, when displaying emotional text on the screen, such as poems can often be, one must take into account certain things that human readers often do naturally. For all creative arts, pauses are one very important factor – imagine a really beautiful poem scrolling on the screen without any dynamic variance whatsoever – not much fun to read, is it? For the format used in this tutorial program, we have added the possibility of adding fixed-length pauses by inserting the ‘^’ character into the poem text. The reason is simple: we need a newline at the end of each line, but we also need a longer pause after each verse (that’s where you’d normally insert an extra newline). This is what TSCText->Parse() takes care of. If you can’t understand what (char)*(*Row + i), does then here’s a little explanation: Row is dereferenced, making it a standard string. Then, an offset within the string is specified by adding i to it. Finally, the offset at which we arrived by adding i to the initial address of the string, is dereferenced again, making it a simple character, which is then assigned to the next character in the array we called Text. If you don’t understand how this works, try reading a couple of tutorials on pointers and pointer arithmetic. |
}
else
WinError(" Cannot execute the specified query. Please check the syntax and semantics.\n"
" Possible causes: the query is invalid, the database and/or table does not exis\n"
" exist at the specified IP address/port. ");
}
|
The else block simply notifies the user if the query cannot be completed successfully. This brings us to
a very important thing that should be kept in mind when creating a screensaver. Remember, we earlier
made note of the fact that in the user should never be prompted for any kind of input by the
screensaver. Well, this is one place where we have ignored this rule: we’re calling WinError() on three
distinct occasions in this function which means that we make absolutely sure that the user is notified if
anything goes wrong. This was added for debugging reasons – when writing an end product, you
should handle these cases smartly, but not bother the user!
This concludes the SQL-related stuff needed for this tutorial! The above code is very simple – it is simply something to get you started. If you need to use advanced stuff then MySQL online help is the way to go. System callbacks and subclassing We will now be moving on to the callback function for Configuration dialog, described earlier. |
// this is the system callback (check out the return type - it has WINAPI in it!)
// for the screensaver. This callback is subclassed by the main window (TGLWindow).
// Either of these instances handle different system messages - see the TGLWindow
// class for a more comprehensive explanation.
LONG WINAPI ScreenSaverWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
// exit if the user presses a key or Alt + F4 (or there's some other indication
// that the program should be terminated)
switch(uMsg)
{
case WM_CLOSE:
case WM_KEYDOWN:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
|
Okay, here’s where the confusion starts with the subclassing and the TGLWindow class. For a good tutorial
on subclassing and writing a window wrapper class, check out [5]. To save some valuable time, let’s go
over this briefly here: as mentioned, each window has a callback function that is maintained by
Windows – a place that is both accessible to the programmer and to the internal workings of the
operating system. That’s the place that is used to notify the programmer of changes that take place in or
apply to a window. In some instances, you might want to have multiple instances of the same function,
such as in this case. This means that you need to subclass the window’s callback function. This is done
using the functions SetWindowLong() and GetWindowLong() – check out the source code, more specifically the
TGLWindow::Create() function in TGLWindow.cpp for further explanation. This means that we pass a new
memory address to Windows where we expect to catch the very same messages that are normally sent
to the primary window callback. In this code example we have a very clear example of this: we handle
all mouse input in TGLWindow:: GLWindowProc() (we really do handle them since we return 0. If we were to
return CallWindowProc() as is done for all other messages that are sent to the user, these messages wouldn’t
be handled) and only handle the termination conditions in ScreenSaverWindowProc(), even though these two
are essentially the same. To get a feel of this, you could do the following experiment: add a MessageBox()
call to one of the WM_MOUSExxx messages in TGLWindow:: GLWindowProc(), run the saver and press the mouse
button you chose to handle. Then move the MessageBox() and the WM_MOUSExxx message handler to
ScreenSaverWindowProc(), and run the saver again. Now click the mouse button and you’ll see! Now try
returning 0 in one of these functions for that specific mouse message handler and see which of those
functions is called first by Windows. It is important to note that handling all mouse input inside the
TGLWindow class is absurd – it is there simply to show you how the concept of subclassing works.
The Configuration dialog So, what next? The contents of configure.cpp – namely the configuration dialog. This will lead to (arguably) the messiest part of this tutorial – the Windows registry. So, without any further ado, let’s get a’crackin’! We will go over the entire contents of configure.cpp: |
#include "main.h" #include "def.rcd" #include <commctrl> // we need commctrl.h for managing TBM_xxx messages #include <windowsx> // we need windowsx.h for the GET_WM_COMMAND_ID() macro
| commctrl.h will allow us to use tracker bar messages (synonymous to the slider bar in this context) that start with the TBM_ prefix. We have two of those controls on the configuration dialog. |
// declare the handles for all of the controls in the dialog box HWND SpeedHandle = NULL; // tracker bar HWND ScatterHandle = NULL; // tracker bar HWND ScatterLEDHandle = NULL; // static text HWND SpeedLEDHandle = NULL; // static text HWND ServerIPHandle = NULL; // edit box HWND ServerPortHandle = NULL; // edit box HWND DatabaseNameHandle = NULL; // edit box HWND UsernameHandle = NULL; // edit box HWND PasswordHandle = NULL; // edit box HWND SQLStatementHandle = NULL; // edit box
| Here are handles to all of the interactive controls on the Configuration dialog (there are four more static texts and two group boxes and three buttons, but we’re not using them interactively with the exception of the buttons – we’ll go over these below). |
BOOL WINAPI ScreenSaverConfigureDialog(HWND hDlg, UINT message, UINT wParam, LONG lParam)
{
// some needy variables
int Value;
char str[12];
// handle the messages
switch(message)
{
// upon creation of the dialog
case WM_INITDIALOG:
// acquire the correct handles from the resource (see Screensaver.rc)
SpeedHandle = GetDlgItem(hDlg, IDC_TEXT_SPEED);
SpeedLEDHandle = GetDlgItem(hDlg, IDC_TEXT_SPEED_LED);
ScatterHandle = GetDlgItem(hDlg, IDC_SCATTER);
ScatterLEDHandle = GetDlgItem(hDlg, IDC_SCATTER_LED);
ServerIPHandle = GetDlgItem(hDlg, IDC_SERVERIP);
ServerPortHandle = GetDlgItem(hDlg, IDC_SERVERPORT);
DatabaseNameHandle = GetDlgItem(hDlg, IDC_DATABASENAME);
UsernameHandle = GetDlgItem(hDlg, IDC_USERNAME);
PasswordHandle = GetDlgItem(hDlg, IDC_PASSWORD);
SQLStatementHandle = GetDlgItem(hDlg, IDC_SQLSTATEMENT);
| The above block does one very simple thing – it simply retrieves the proper handles for the controls on the Configuration dialog upon the initialization of the Configuration dialog. |
// set the range of the text speed to [25, 250]
SendMessage(SpeedHandle, TBM_SETRANGE, true, MAKELONG(25, 250));
// set the step size when moving the slider on the tracker bar
// (it now increments by steps of 25)
SendMessage(SpeedHandle, TBM_SETPAGESIZE, 0, 25L);
// set the position of the slider to a predefined (preloaded)
// value (stored in T)
SendMessage(SpeedHandle, TBM_SETPOS, true, T);
// set the led to reflect the value
sprintf(str, "%i", T);
SendMessage(SpeedLEDHandle, WM_SETTEXT, 0, (LPARAM)str);
| This and the following group of function calls are very similar and will be explained only once. First of all, we know that everything in Windows is done by sending/handling messages and notifications. To send a message to a control, WinAPI provides us with the function SendMessage(), that takes four parameters: the handle of the receiving control, the message itself and two additional parameters that can be used to specify or receive additional values. SendMessage() also returns LRESULT (a 32-bit value). By sending specific messages to a control, we can define its behavior rather precisely. In this case we want to make our slider bars obey certain limitations: we want to specify the range (25 – 250 for the text speed and 0 – 250 for the scatter value; note that we’re using a multiply of 10 of the used scatter value on the tracker bar; we do this by sending the TBM_SETRANGE message to the appropriate controls), the page or step size (25 for the text speed and 5 (or .5f in the actual used value) for scatter; this is done by sending the TBM_SETPAGESIZE message) and the current position of the slider on the tracker bar (note that these values are specified in ReadRegistry() in registry.cpp that is called from WinMain(); we do this by sending the TBM_SETPOS message – for the scatter value this means multiplying the actual value by 10!). Last, but not least we update the value indicators (controls that contain the word "LED" in their handle name) to reflect the current value in the static text boxes underneath the sliders. We do this by sending them the text using the WM_SETTEXT message (notice the WM_ prefix which makes this a generic window message – e g can be sent to most controls). |
// do the same for the scatter value, this time with a small exception // in mind: we will later on be using values ten times smaller than // those indicated by the slider bar. That is, if the slider is moved // all the way to the end (eg 250), we will be using the value 250 / 10 // or 25.0 internally instead. Can you figure out why? Hint: see help // on the TBM_SETRANGE message SendMessage(ScatterHandle, TBM_SETRANGE, true, MAKELONG(0, 250)); SendMessage(ScatterHandle, TBM_SETPAGESIZE, 0, 5L); SendMessage(ScatterHandle, TBM_SETPOS, true, Scatter * 10); sprintf(str, "%.2f", Scatter); SendMessage(ScatterLEDHandle, WM_SETTEXT, 0, (LPARAM)str); sprintf(str, "%i", ServerPort); SendMessage(ServerPortHandle, WM_SETTEXT, 0, (LPARAM)str); SendMessage(ServerIPHandle, WM_SETTEXT, 0, (LPARAM)ServerIP); SendMessage(DatabaseNameHandle, WM_SETTEXT, 0, (LPARAM)Databasename); SendMessage(SQLStatementHandle, WM_SETTEXT, 0, (LPARAM)QueryText); SendMessage(UsernameHandle, WM_SETTEXT, 0, (LPARAM)Username); SendMessage(PasswordHandle, WM_SETTEXT, 0, (LPARAM)Password);
| This last section of WM_INITDIALOG sets the text values for all of the other controls on the dialog. |
return true;
case WM_CLOSE:
PostQuitMessage(0);
break;
case WM_COMMAND:
switch(GET_WM_COMMAND_ID(wParam, lParam))
{
case IDCANCEL:
PostQuitMessage(0);
break;
case IDUPDATE:
UpdateRegistry();
PostQuitMessage(0);
break;
case IDREMOVEFROMREGISTRY:
RemoveRegistry();
PostQuitMessage(0);
break;
}
| If we’re sent a WM_COMMAND message, we can expect input from any of the button controls that are children of the Configuration dialog. There are three of those: "Exit", "Update and exit" and "Remove all registry entries". The "update" in "Update and exit" insinuates that something will be updated – this something is the Windows registry. See UpdateRegistry() below. Upon pressing "Remove from registry", the one single key created by the saver is deleted from the registry – see RemoveRegistry() below. The GET_WM_COMMAND() macro conveniently combines the lParam and wParam and gives us the handle of the control which the message is coming from. |
return false;
// if the user scrolled something (anything) on the dialog box, we're
// notified through the WM_HSCROLL and WM_VSCROLL messages. We only
// have horizontal scrollbars
case WM_HSCROLL:
// the handle to the tracker bar is passed to use as lParam, so all
// we need to do, is match it
if((HWND)lParam == SpeedHandle)
{
// get the position of the tracker bar
Value = SendMessage(SpeedHandle, TBM_GETPOS, 0, 0);
// make it a string
sprintf(str, "%i", Value);
// store the speed value in T (global variable)
T = Value;
// set the LED (the number underneath the scrollbar) to reflect
// the current value
SendMessage(SpeedLEDHandle, WM_SETTEXT, 0, (LPARAM)str);
return false;
}
// do the same for the scatter parameter
else if((HWND)lParam == ScatterHandle)
{
Value = SendMessage(ScatterHandle, TBM_GETPOS, 0, 0);
// normalize the number by dividing it by 10 - makes the number
// seem a little more user friendly
sprintf(str, "%.2f", Value / 10.f);
Scatter = Value / 10.f;
SendMessage(ScatterLEDHandle, WM_SETTEXT, 0, (LPARAM)str);
return false;
}
| The last message we handle is the WM_HSCROLL message sent when "something horizontal" takes place on the parent window. This can be when the user scrolls the window horizontally using the scrollbar or, as in this case, when the user moves the indicator on either of the tracker bars. Looking up the WM_HSCROLL message (on MSDN, for instance), will yield that the handle of the control is sent to us as lParam. Now, all we have to do is match the handle with the correct control handle that we already know and we also know what to do next. For either of the slider bars, we first retrieve the position of the slider by sending the control the TBM_GETPOS message. We then update the LED below the slider bar and store the value. Simple, isn’t it! |
} return false; }
|
Tampering with the registry
The Windows Registry Since we’re dealing with a screensaver and we don’t want to keep the settings we use in a config or ini file, we store them in the safest place imaginable – the Windows registry. Unfortunately, doing so requires some understanding of how things work. You can open the registry using the Run... command in the Start menu. Just type "regedit" in the box and press OK. The registry is displayed. A word of caution: this is serious stuff and tampering with wrong variables can render your operating system crippled or unusable! We will only be dealing with one item in this tutorial. When you first open the registry, you will notice that what we’re dealing with is essentially a simple tree. This is actually the best way to think of it – everything in it obeys to hierarchy. As the first thing, collapse all open nodes in the tree so that you’re left with only five visible braches. One of these branches is called HKEY_CURRENT_USER – this is the one we will be dealing with. As the name suggests, this branch is called a key – in fact all yellow folders in the tree are called keys. Each key can have several values attached to it. We will get to those later. Now, expand HKEY_CURRENT_USER. A bunch of keys pop into the view. Seek out the one with the name "Control Panel" and expand it. Looking through the list that appeared, you should be able to find several keys with the words "Screen Saver." in them – this is where Windows screensavers keep their settings, so we will store our settings here, too (under the name "Screen Saver.OpenGL Poem"). If you expand any of these keys, you will be able to see several values on the right side. There are three columns: Name, Type and Data. The Name and Data fields are self-explanatory, but the Type field can be slightly confusing. More on this later. Knowing this, we will go through registry.cpp top-down. |
#include "main.h" // takes a 4-byte string and converts it into a 32-bit decimal number using // WinAPI macros #define MAKEDEC(a) MAKELONG(MAKEWORD(a[0], a[1]), MAKEWORD(a[2], a[3]));
|
We will be using this macro later on – it utilizes standard WinAPI macros to create a 32-bit variable
out of four 8-bit ones (a 4-byte string).
We will now look at how to write into the registry. Creating a new key and adding values to it |
void UpdateRegistry()
{
HKEY RootKey = NULL;
HKEY ScreensaverKey = NULL;
| In order to write into a key or create one, the parent of that key/value must exist and be opened with appropriate rights. Therefore, in order to create our screensaver’s key as the subkey of "Control Panel", the latter must be open. When opening a key successfully, we acquire and handle to it that is of type HKEY. We will be storing the handle to the parent ("Control Panel") in RootKey. |
if(RegOpenKeyEx(HKEY_CURRENT_USER, "Control Panel", 0, KEY_ALL_ACCESS, &RootKey) != ERROR_SUCCESS)
ShowError("UpdateRegistry() -> RegOpenKeyEx(RootKey)");
| We test if we can open the key using RegOpenKeyEx(), the extended 32-bit version of RegOpenKey(). The first argument is a previously opened key handle or one of the five classes you can see in the Registry editor. The depth of the tree we’re opening here is only one level deep; therefore the second argument only has one level in it. For instance, we could also open three levels at a time like this: "Control Panel//Desktop//WindowMetrics". The third parameter is reserved and must be zero; the fourth specifies the access rights we want to have. You can read up on these on your own – we’re simply granting ourselves all possible access. The final parameter receives the handle for the opened key. We will be using this in the following calls. |
RegCreateKey(RootKey, "Screen Saver.OpenGL Poem", &ScreensaverKey);
|
We create a new key called "Screen Saver.OpenGL Poem" under RootKey and receive a handle for it in
ScreensaverKey. If the key already exists, RegCreateKey() opens it instead.
Next we create a bunch of values under ScreensaverKey using RegSetValueEx(). Some of the code has been removed (look at the source code for the full listing) and replaced with three dots. This is done to avoid excessive repetition. Some notes on the following function calls, more precisely the fourth parameter: in this tutorial we’re only using two different data types (there are many more) for our variables in the registry – the string (REG_SZ) and the long (REG_DWORD) data types. This is the main reason why the scatter value is multiplied by 10 – to make it a valid DWORD that can be read directly from the registry. The first argument of RegSetValueEx() is the handle to the key whose values we want to modify; the second argument is name of the data field; the third argument is reserved and must be 0; the fourth one specifies the data type of the value and the fifth one specifies the value we want to set the value field to. The last parameter specifies the size of data in bytes that we’re storing in the registry. |
if(RegSetValueEx(ScreensaverKey, "Text speed", NULL, REG_DWORD, (const BYTE*)&T, sizeof(DWORD)) != ERROR_SUCCESS)
ShowError("UpdateRegistry() -> RegSetValueEx(Text speed)");
DWORD Scat = Scatter * 10;
if(RegSetValueEx(ScreensaverKey, "Scatter speed", NULL, REG_DWORD, (const BYTE*)&Scat, sizeof(DWORD)) != ERROR_SUCCESS)
ShowError("UpdateRegistry() -> RegSetValueEx(Text speed)");
char StringValue[256];
ZeroMemory(StringValue, 256);
SendMessage(ServerIPHandle, WM_GETTEXT, SendMessage(ServerIPHandle, WM_GETTEXTLENGTH, 0, 0) + 1, (DWORD)StringValue);
if(RegSetValueEx(ScreensaverKey, "Server IP", NULL, REG_SZ, (const BYTE*)StringValue, strlen(StringValue)) != ERROR_SUCCESS)
ShowError("UpdateRegistry() -> RegSetValueEx(Server IP)");
| In order to get the proper string to store in the registry, we must retrieve it from the correct control (edit box) first. We’re using a neat convention here – calling embedded SendMessage() commands: the outer one to retrieve the text and the inner one to specify the outer one’s length argument. Note that we’re adding a 1 to make up for the trailing null character. |
… RegCloseKey(RootKey);
| When we’re done, it’s always nice to close the door. |
RegFlushKey(ScreensaverKey);
| By calling RegFlushKey() we force the Windows registry to update. This is not something that should be done without thinking through the nature of the program first since it is a procedure that occupies a great deal of system resources. We don’t have any limitations on time here, but for larger registry modifications this could take up several seconds! |
}
|
Reading from the registry
In this section we won’t be delving into the code that much since it only differs from the code described under the previous section by a marginal amount. Instead we will go over the key bits of the code (no pun intended) in ReadRegistry() in registry.cpp. First of all, as in the case of writing, we need to open the key we want to read from using RegOpenKeyEx(). This will give us a handle to the registry key we’re about to read from. We store this key in RootKey. In the code, if we fail to open the key, we assume that there is no registry entry of the predefined name and we automatically assign some default values to the proper variables. Next comes the trickier part. The code looks like this: |
DataSize = 1024;
// read the text speed
if(RegQueryValueEx(RootKey, "Text speed", NULL, &DataType, DataValue, &DataSize) != ERROR_SUCCESS)
ShowError("ReadRegistry() -> RegQueryValueEx(Text speed)");
T = MAKEDEC(DataValue);
|
We use RegQueryValueEx() to query a value from the key for which we have stored a handle in RootKey – that
is the first parameter. The second parameter is the name of the value field and the fourth one must be
null. The last three parameters are as follows: a buffer for the type of the value field, a buffer for the
actual data and the size of the data returned. The catch here is to specify DataSize beforehand and pass
RegQueryValueEx() a large enough value to actually hold the buffer. We’re not doing this in the current
tutorial, but it would be a lot more correct to first run the query with DataType and DataValue as NULL and
only query the size of the underlying data, and then call RegQueryValueEx() again as shown above. We’re
assuming (something that’s definitely not a good thing) that DataSize = 1024 is large enough to hold the
returned DataValue. After the function returns, DataSize holds the actual size of DataValue as opposed to the
original 1024. Does that make sense?
The last line simply takes a string as its argument and converts the first four characters to a 32-bit number. Note that DataValue can contain any data type when RegQueryValueEx() returns – from an ASCII string to an unsigned long to pure binary data – therefore this argument has to be something very generic, e g a BYTE*. Deleting a key from the registry Again, this isn’t much different from writing into the registry – the key we want to delete has to be opened first using RegOpenKeyEx(). Here’s the code: |
int RemoveRegistry()
{
HKEY RootKey = NULL;
char* SubKeyName = "Control Panel\\Screen Saver.OpenGL Poem";
if(RegOpenKeyEx(HKEY_CURRENT_USER, SubKeyName, 0, KEY_ALL_ACCESS, &RootKey) == ERROR_SUCCESS)
if(RegDeleteKey(RootKey, NULL) != ERROR_SUCCESS)
{
ShowError("Could not remove the registry entry");
return -0x1;
}
return 0x1;
}
| The key is deleted via the RegDeleteKey() function that takes two arguments: the handle to the key and the name of its subkey we’re about to delete. Therefore, we could easily have written it as: |
RegDeleteKey(HKEY_CURRENT_USER, SubKeyName);
|
It’s as simple as that.
This concludes our detour into the Windows registry – hopefully it has been useful and you’ll be taking advantage of its positive features (such as storing simple, but essential, values). Just don’t crowd the registry with senseless info – such as binary data for images or sounds – this will end up having an impact on the performance of the OS. As the final part of this tutorial, we will have a look at the TGLFont and TGLWindow classes and how OpenGL fits into all of this. TGLWindow, TGLFont == OOP, or, is this the way to go? The name of this section suggests a fierce battle and most likely sends shrills down some of the patriots of procedural programming. This won’t be a section that will try to justify using a fully object-oriented approach in your OpenGL projects, but it will give a short overview of the benefits of this kind of modularization. No source code here, just some thoughts… If you have been feeding off of simple tutorials that only use minimal code to create a window and draw some rotating quad or something, then this isn’t something you should be too concerned about just now. However, if you have been trying to create a larger project and become totally entangled in the ever-growing amount of source code, OOP is definitely something to consider. The key word always used when describing the term OOP is encapsulation. The other terms that should cross your mind when somebody mentions OOP are: reusability, inheritance and polymorphism. Consider [6] or [7] (Thinking in C++, 2nd ed.) for reading up on these terms. In the case of OpenGL, such kind of encapsulation also serves a slightly different purpose, though. Since OpenGL, just like DirectX, is an API that relies on the existence of a target HWND, it can be said that the window is effectively used to channel all audio/visual dataflow. This makes harnessing the HWND object a very powerful tool indeed. One of the great things about the TGLWindow class has already been addressed previously in this tutorial – the capturing of the rendering context and automatic buffer swapping. A far more powerful feature, however, hasn’t been addressed in this tutorial. Actually, this can be split up into two distinct problems, both of which can rather easily be solved with the help of OOP. These are:
Now a few words about the font class: in this case there is no actual benefit in creating the font class since it is not very large. You should, however, keep in mind that none of the classes enclosed with this tutorial are as large as they will ever get – in fact we have minimized their functionalities to leave room for growth and exploration. Some ideas that you might want to consider tackling on your own: what about a menu for the window? What about minimize/maximize/restore? Or automatic resizing of the OpenGL view when the user resizes the window (yes, this is not included!)? Browse through the source code for comments on topics not discussed here – there’s a lot that has been left for you to explore on your own. The incorporation of OpenGL into all of this might seem vague, but then again, OpenGL is simply a tool – one of the many out there. It isn’t only the visual side and the neat special effects that make good software, but the blending of many features from many different fields. This tutorial tries to demonstrate that. It is only unfortunate that a lot of it is not thoroughly explained – that makes this tutorial an advanced one. The conclusion In conclusion, while this tutorial is largely based on NeHe’s previous tutorials, a lot has been changed and some very important things left out. I hope you have enjoyed this lengthy rundown of the essentials and hopefully picked up a few useful tips. If you have any suggestion, complaints or ideas (or you just wish to share your thoughts on the subject), my e-mail is christopheratkinson@hotmail.co.uk. Resources 1 - http://www.mysql.com 2 - http://www.cs.unibo.it/~ljw1004/download/howtoscr.html#ScrPrevCode 3 - http://www.alexchirokov.narod.ru/HOWTOSCR.HTM 4 - http://www.mysql.com/doc/en 5 - http://www.gamedev.net/reference/articles/article1810.asp 6 - http://www.embedded.com/97/fe29712.htm 7 - http://www.mindview.net/Books/TICPP/ThinkingInCPP2e.html * DOWNLOAD Visual C++ Code For This Lesson. * DOWNLOAD Code Warrior 5.3 Code For This Lesson. ( Conversion by Scott Lupton ) |