Click here to Skip to main content
Click here to Skip to main content
Go to top

A DirectX Game: Quadrino

, 16 Oct 2008
Rate this:
Please Sign up or sign in to vote.
An interpretation of a popular falling block game implemented with DirectX that attempts to avoid any copyright infringement.

Introduction

I have developed and present to you, a complete 2D game that utilizes functionality up to DirectX 8.0. The game is similar to a classic that can be spelled with the letters 'T' 'E' 'R' 'I' and 'S'. I originally implemented my version of this game in 2002. Because of copyright infringement and pressure from the lawyers at a company whose name can also be spelled with the letters 'T' 'E' 'R' 'I' and 'S', Code Project was forced to remove the original article from its site in December of 2007. However, because of US Copyright law I am able rename my game "Quadrino" and now it can return to Code Project.

I have updated the first two paragraphs, but I will leave most of the original article intact. Most of the changes are superficial including changing all references of the previously hinted name to "Quadrino." I have gone through the source, and created a new project that uses Visual Studio 2005, and I have removed all references in the source code to the forbidden name as well. In making these changes, I have gotten back into my game, and I have fixed a few bugs. I hope to update the graphics, and even add a few more features in the near future.

For those that remember the original article, here is the original screen shot. I have pixelated the image in order to avoid any further harassment for my efforts:

Original Article

I have been interested in computer graphics for quite some time now, and I thought it was time to learn DirectX. I have developed my own set of wrapper classes for the portions of DirectDraw7 that I have used (DirectX 8.0 no longer supports DirectDraw interfaces, so I needed to use DirectDraw7 for the 2D graphics engine.) These classes are loosely based on both the DirectX SDK samples and the chapters for DirectX in Feng Yuan's book Windows Graphics Programming. I chose to create my own framework for a number of reasons:

  1. Certain features that I liked or wanted were not present in these classes.
  2. I saw certain weaknesses in the design, layout or usability in some of these classes.
  3. I thought that I would learn more by rewriting and expanding these classes rather than simply re-using them.

The graphics with DirectDraw are only one portion of the game, The game supports both windowed and fullscreen mode. The windowing system utilizes WTL. For this I needed to develop a customized version of CMessageLoop, I called this CGameLoop, and it is another article I wrote on CodeProject. I have modified the sample classes for DirectSound that have been included in the SDK and created my own classes to manage sound effects. Finally, I wanted background music, but sending a 30 MB sound file over the Internet wasnt feasible, and I do not have enough money to send everyone a soundtrack on CD. Therefore I created a simple class that utilizes DirectShow, that will load and play MP3s. This degrades the performance of the game, but on machines which speeds are over 1 GHz, the difference is neglible.

Requirements

Your video card must support 24-bit or 32-bit color modes at 800x600 resolution. I have recently configured the game to detect the different modes. If the game cannot set your video card to either 32-bit or 24-bit color, it will notify you of this error and exit.

In order to compile the source code you will need WTL, and the DirectX 8.0 SDK.

History

Below I have listed all of the updates that have been performed on this article and the code since it was originally posted, and the date that I made the change. If a member of CP has reported the bug to me or given me a tip to improve my code I also include their name in parenthesis next to the updated item.

October, 2008

Article is reposted with a name change as well as a few bug fixes.

  • Source project converted to Visual Studio 2005
  • Application now uses UNICODE as default character width, code has been updated to accomodate both multi-byte and UNICODE compilations.
  • Time variables use double type all through code rather than downcast to FLOAT. This has increased the smoothness of the animations.

December, 2007

Article removed from CP due to legalities.

November 1, 2002

The source code has changed, and there is a new compiled executable. It is not necessary to re-download the resource files.

  • Removed all of the hardcoded paths found in the code. This was mainly the sound files that were hardcoded. (Tim Smith)
  • The DSSoundManager::CreateSound now initializes the output pSound datamember if the function fails to create the sound object. (Tim Smith)
  • Fixed the formatting for the list of commands in this article.
  • The game now automatically attempts to set the video card into 32-bit mode, and if that fails then tries 24-bit color mode. Previously this required two builds to run the two different color modes.
  • Resampled a few of the sounds so the overall download is smaller now, about 500 KB.
  • Removed absolute path definitions from the project settings. (Tim Smith)

The Game

I realize that a number of people will just want to try out the sample and see if it is worth spending the time looking at the source code. So I will first present the game, the rules and controls, then I will explain how it works in later sections. You will need to download the Demo / Resources files at the very least. This zip file holds all of the graphics, sound files, and the MP3 soundtrack as well as a compiled version of the game. Then either download the precompiled exe or the source and compile the exe yourself. All of the zip files should be extracted to the same directory in order to make the project work. These zip files will create their independant directory structures for the game.

There are three preview pieces that will display the next three pieces in the order that they will be put into play. There is also a single place called the store. The store allows you to hold any one piece that comes into play until you want to use it. When you decide to use it, you can swap out the current active piece, for the piece held in the store.

When you clear four lines with a single piece, you will score 5 points for those 4 lines rather than 4, that is called a Tetr, er, Quadrino. The Quadrino is only possible with the straight line piece. In this version of Quadrino, this move is really down played because of the next feature, which is one that I really like.

The last feature in Quadrino is the creation of "Super Blocks". Super Blocks are created by grouping 4 blocks together to form a 4x4 square. If you use a combination of the 7 types of pieces to create the superblock, then a silver super block will be created. Each line that is cleared that contains a silver super block will be worth 5 points rather than 1. If you are able to use 4 of the same type of blocks, then a gold super block will be created, were each cleared line will be worth 10 points.

There are a number of different ways to create a super block, here are a few examples to get you started:

These formations will create gold blocks:
  

These formations will create silver blocks:
   

You will be able to follow the commands on the title and options screen in order to start the game. For the game play there are just a few simple commands:

Command Description
A Rotate the current piece 90 degrees counter-clockwise.
S Rotate the current piece 90 degrees clockwise.
Space bar Swap the current piece with the piece located in the store. The pieces can only be swapped once for each new piece.
Left Arrow Moves the current piece one column to the left.
Right Arrow Moves the current piece one column to the right.
Down Arrow Moves the current piece one row down.
Up Arrow Automatically drops the piece to the position that is indicated by the shadow piece.
P Pauses the game.
Alt + F11 Toggles the game between full-screen mode and windowed mode. Full-screen mode is the default.

Design

The game is built up from a number of smaller sub-systems. Each system provides a distinct purpose. I have identified the systems below.

  • Game Engine
  • Main Window
  • Quadrino View
  • DirectDraw wrappers
  • Sound Manager
  • Music Player
Each of these systems is described below in greater detail, including the role that it provides and how it is implemented. These systems are currently only described at a high level. I plan on providing more detail for these sections in the future depending on the interest that is shown for this article.

Before we get started into the separate sub-systems, I would just like to say a few words about my implementation. All of the images are loaded using external settings from an ini file. I did this because I wanted a way to make multiple levels for the game. I have only included one with this version, but maybe I will expand it later and provide more levels. Also, many of the settings are simply recorded as constants in the header files. The settings for screen size, board size and other constants are defined there as well.

Game Engine

I started with the game logic before I wrote a single line of display or sound code. The heart of the game is located in a class called CQuadrinoGame. This class contains all of the logic to manage a Quadrino game. It should be possible for you to take this class, and create your own graphical representation of the game using this class.

Internally CQuadrinoGame creates another class called CQuadrinoBoard. The Quadrino board is repsonsible for managing all of the data that is associated with the board. This includes where the pieces are currently set into place, and the state of the completed lines. Collision detection, for the active peice movement, is also performed by the CQuadrinoBoard class.

The data for the pieces is defined in Pieces.h and Pieces.cpp. The pieces are defined inside a set of predefined structures. There is one piece definition for each of the four directions that a piece can be oriented. Here is the definition for the structure:

struct Quadrino_PIECE 
{
	BYTE	cells[4][4];
	BYTE	bottom[4];
	POINT	offset;
	SIZE	size;
};
  • cells: The cells is a 4x4 array, that indicates where a block is placed for the current orientation of the piece.
  • bottom: Indicates where the bottom of the piece is located in the cell array. This field provides information for collision detection.
  • offset: Indicates upper-left corner of the bounding box for the piece in the arrangement. This helps calculate the movement of the piece and with collision detection.
  • size: Indicates the size of the bounding box for the current piece.

The pieces are stored in a large multi-dimensioned array that is called g_Pieces. Constants are defined to located each piece in this array. Listed below are the constants that are defined and their meaning:

  • PT_EMPTY: No piece
  • PT_LINE: The line piece, or many call it the Quadrino piece.
  • PT_LEFT: The piece that looks like a backwards "L". The bottom portion points to the left.
  • PT_RIGHT: The piece that looks like a "L". The bottom portion points to the right.
  • PT_T: The piece that looks like a "T".
  • PT_BLOCK: The square piece this is a 2x2 block.
  • PT_Z: The piece that looks like a "z".
  • PT_S: The piece that looks like a "s".
  • DIR_0: Indicates a piece in its default postion at 0 degrees.
  • DIR_90: Indicates a piece rotated 90 degrees.
  • DIR_180: Indicates a piece rotated 180 degrees.
  • DIR_270: Indicates a piece rotated 270 degrees.

Here is a sample definition for the piece that is shaped like a "T" oriented at 0 degrees:

g_Pieces[PT_T][DIR_0] =
{
	{
		0, 0, 0, 0,
		0, 0, 1, 0,
		0, 1, 1, 1,
		0, 0, 0, 0
	},
	{4, 2, 2, 2},
	{1,1},
	{3,2},
};

Main Window

The main window manages the game. This manages the frame (in windowed mode), the user input, and the display. I have created a custom message handler class for WTL that works with the CGameLoop class that I created. My main window is derived from the game handler class in order to get notifications and provide feedback to the gameloop. When the gameloop does not have any messages left to process, it calls OnUpdateFrame in the Main Window.

OnUpdateFrame

This function is basically where the game is processed. This function is somewhat simple, it does two things, first it processes any input with the help of DirectInput, then it updates the graphics. Both of these tasks are delgated to other functions to handle the more detailed aspects.

The current mode of the game depends on the message handler that I use to process input. If the user is on the title or gameover screen, I use the main screen input. If the user has paused the game, then the pause screen handler is used. And finally if the game is currently active, then the game input handler is used. Sound is also played when certin input events are triggered. The sound manager is described in a later section.

All of the graphical screen updates are done in the Quadrino view class. The Quadrino view class is a child window of the Main Window. The QuadrinoView has a member function called UpdateFrame. This function will update the display based on the current state of the game.

Quadrino View

The CQuadrinoView class is a window that manages all of the graphical output of this game. This window is derived from a class that is modeled after the DirectX SDK game window class. However my version works with WTL. The class that created is called DDrawWindow. This class provides all of the basic functionality needed to create your DirectDraw primary buffer, initialize the clipping surfaces, and switch between windowed and screen mode.

CQuadrinoView is where most of the work went for the game, (aside from the DirectDraw wrapper classes.) The basic organization of this class involves a set of drawing surfaces that cache the different portions of the display. For instance the background, game board and the pieces. Each of these things is cached, and the state of its display is remembered. When an update is requested, the state of these cache surfaces is checked, and if the surface needs to be updated, only then will it be painted.

Double Buffering

A Double buffering systems has been implemented to manage the display. Rather than creating a true sprite animation engine, I have chose to simply use three surfaces. One surface is a static back-buffer, which is called m_pUpdateSurface, that rarely changes. I paint the background, the board, the score and other things on this surface. Every frame this surface is blitted to the back buffer, which is called m_pBackSurface. This surface is where I paint all of the current frames of animation that is ready to be displayed. The final surface is the primary display surface.

I never write to the primary surface directly (however the DDrawWindow base class does this internally). In order to get the back-buffer data to display on this surface, I call the Flip function of the CQuadrinoView class. The flip function determines which mode the game is currently in, and flips the buffers appropriately. In windowed mode, the best that can be done is a blit from the back buffer directly to the primary surface. However in full screen mode, page-flipping can be used which is a direct manipulate of the memory in the video card. I generally see a double in display performance while in full screen mode. While in debug mode I display the current frame refresh rate for the game. On my machine I see 25 frames / second in windowed mode, and 60 frames / second in full screen mode.

Painting the Board

I have created a cache of the board image. The board is 10 blocks wide by 20 blocks high. Each block is 20x20 pixels, therefore my board surface is 200x400 pixels. However, these definitions are all declared constants so these dimensions can be easily changed. I only repaint the board when the Quadrino Game class indicates that one of the lines have changed. A line can change only when a new piece has been set into place, or a row of blocks collapse because lines have been cleared.

Currently I only paint the lines of the board by color filling the region that the lines occupy, then drawing a border around the edge of the blocks. When a line gets cleared, I need to redraw the line that is removed, as well as the lines that are above and below the lines that have been cleared. This is to add a border to the blocks that are at the edge of a line that has been removed.

Something that I wanted to do, but that I did not anticipate was displaying a texture as the set peice rather than simply using a color fill to paint the blocks. This would require a little bit of state information that I had not set into place. This will hopefully be an enchancement that I add to the game in the future.

Animations

I decided to use key frame animations for the this game. Key frame animation is where you set key positions for the life of the animation, and interpolate all of the frames in between. I chose this because of the possible variation of the frame rates on different machines. I wanted the sound and graphics to sync up when the animations occur.

The animations are implemented as an independant class. Each class is responsible for generating its own display. Each time a class is intantiated, it is placed in the animation queue, and the frames for the animation are rendered each time the display is updated. Currently animations are only generated when special events occur like swapping the store peice or clearing a line, however I also envisioned adding background animations to this game.

Each animation is derived from the abstract base class called Animation. The Animation class is simple. It provides the means to initialize the animation, set a delay for when the animation should start, the ability to play sounds when an animation starts or ends, and the ability to render the current frame based on time for the animation. Here is a summary of the functions for the base class Animation:

  • IsComplete:Indicates that the animation is complete.
  • GetStartTime:Returns the time that the animation should start at.
  • SetStartTime:Sets the time that this animation will start at. It will remain inactive until the time set has passed.
  • GetTriggerID:Returns the trigger ID that has been associated with this animation.
  • SetTriggerID:Sets the trigger ID for this animation. The trigger ID is a value that indicates to the view events that should occur when the animation completes. Like start the pieces descending again, or refresh the display.
  • SetStartSound:Sets the sound that will be played when the animation starts. If no sound is set, then no sound will be played.
  • SetEndSound:Sets the sound that will be played when the animation ends. If no sound is set, then no sound will be played.
  • RenderFrame:Renders the current frame of the animation. The current time will be taken into consideration when calculating and rendering the current frame.

Shadow Piece

I wanted to quickly comment on the shadow piece that is used to display where the active piece will land if you just let it fall. I implemented this using AlphaBlending. However there is no alphablending feature present in DirectDraw. Therefore I wrote an alphablending function by hand. This works well, however it is a resource hog, and when I tried to blend large numbers of pixels, the frame-rate started to diminish. Therefore I reserved the alpha blending for special occasions, like the animations.

If you know about Direct3D, then you will know that Direct3D provides the ability to have the video card do the alpha blending directly on the video card. That is a feature that I would like to have had, but I was not ready to learn Direct3D and create my wrapper classes for a Direct3d surface when I was developing this game. That will definitely be something that I produce in the future, but that is why it is not present in this game.

DirectDraw Wrappers

More than a year ago, I started writing a 3D pixel shader. I wanted to revisit some old 3D graphics principles that I learned in college, and I wanted to learn DirectX. While I was writing this program I also started to develop my basic set of DirectDraw wrapper classes. The initial design for these classes is largely based on the classes found in Feng Yaun's graphics program book. However as I got further into my project, I started needing things that Feng Yuan either did not provide, or I wanted to do differently.

DDSurface

The wrapper classes that I have created basically abstract a portion of video memory into a class for you to manipulate. This class is called a DDSurface. This is basically a wrapper around the DirectDraw class IDirectDrawSurface7. IDirectDrawSurface7 provides very little functionality. It allows you to Blt surface data to different surfaces and get information about the surface, and the most important feature, the ability to manipulate the bits of the surface directly. This class supports the Blt and BltFast functions in the DirectDraw interface. I have also tried to encapsulate a few common tasks such as color fills.

This class takes care of many of the mundane details that are required to maintain when you are dealing with windowed mode. If you paint on a surface in windowed mode, you need to take into account the location of the client area for the window much the same way you do when painting with DCs in GDI. Therefore if you associate a DDSurface with a particular window, the surface will automatically offset all painting commands to the requested position inside of the window that you have specified. In the future I would like to add the ability to support Viewport scaling as well.

DDBuffer

Before you can edit this bits directly , you need to lock the surface and you will get a memory buffer to manipulate. There is also no ability in DirectDraw to draw primatives like a line, rectangle, ellipse, polygons and so on. You either have to do this yourself, or use GDI. I have created a class called DDBuffer in order to encapsulate this process. DDBuffer will allow you to manipulate the bits directly, as well as draw primitives and other basic graphic functions. I modeled the functions of this class after the functions in GDI, in order to make the systems familiar to already well documented APIs.

Here is a short list of the functions that DDBuffer supports. I am not going to go into detail about these functions as they behave very similar to the GDI functions found in the Win32 API.

  • GetPenColor
  • SetPenColor
  • GetFillColor
  • SetFillColor
  • GetPolyFillMode
  • SetPolyFillMode
  • GetPixel
  • SetPixel
  • MoveTo
  • LineTo
  • Rectangle
  • Polyline
  • PolylineTo
  • PolyPolyline
  • SimplePolygon
  • Polygon
  • GradientFill
  • TriangleGradientFill
  • AlphaBlend

I did not use all of these functions for Quadrino, however I did use and test most of them for my 3D pixel shader that I wrote prviously. All of these functions have not yet been tested for all color depths. I generally use 24-bit or 32-bit color modes, therefore these classes may yet be suitable for 8-bit color modes. However there is no reason that these classes could not be expanded.

DDOffScreenSurface

The offscreen surface is a surface that will allow you to load a bitmap from a file, create a surface of an arbitrary size, or directly from DIB section. Other than that it behaves exactly like a DDSurface object.

DDFontSurface

The DDFontSurface class is a great idea that I learned of in Feng Yuans's book. DirectDraw has no support for fonts either. Therefore if you want font support you either need to create a bitmap based set of character tiles, or resort to GDI to paint your font. I have chosen to create this class to manage my fonts. Upon the construction of this class, you speciify a logfont structure as if you were creating a font in GDI, and this class will pre-render each character that you specify to a DDSurface. Then it can simply be blitted from a bitmap like a normal bitmap tile font.

When you are ready to paint a string of text, simply call DrawText and specify the destination surface, the string, and the destination rectangle. The function will take care of the rest. In the future I would like to expand this class to be able to export the new bitmap font so it can be externally modified, and imported. I will expand the features of this class in the future as I need them.

Sound Manager

This class is very basic, and is closely based on the code found in the DirectX 8.0 SDK. I have changed the way a few of the functions work because I thought it would be more efficient, and I reformatted the code to my preferences. The basic design includes a class that initializes the DirectSound system for a particular window, and a sub-class that manages individual sound.

DSSoundManager

The sound manager class contains three functions of interest.

  • Initialize: Initializes the DirectSound system and associates this instance with a particular window.
  • SetPrimaryBufferFormat: Initializes the format for the primary sound buffer that will play the sounds.
  • CreateSound: The only mechanism that will load a create a new Sound object.

DSSound

Manages a single sound. The sounds that are supported are wave files. With this class you can load and manage individual wave file sounds.

  • Initialize: Loads and initializes a new sound buffer. If you would like more than one instance of the current sound to be played concurrently, then you can simply specify multiple buffers for this sound in initialization. The number of buffers that is specified is the number of times this sound can be played concurrently.
  • Play: Plays the current sound.
  • Stop: Stops the sound from playing
  • Reset: Resets the sound buffer to the beginning.
  • GetVolume: Returns the current volume level for this sound.
  • SetVolume: Sets the volume level for the current sound. The default model for sounds in DirectSound range from -10000 to 0. I simplified this by making the range go from 0-100%.

Music Player

One thing that I thought would be a great addition to my game was the ability to play a sound track. The only way that I could see doing this over the internet was to use MP3 files. I was pleasantly surprised to find out how simple it is to play an MP3 file using the interfaces in DirectShow.

I have developed a simple class that will load and play an MP3 file. This class is called MusicPlayer, and it provides about 10 functions that allow you to use it. Here is a description of the most important functions:

  • SetCurrentFile: Sets the path to the current file that should be played. You can basically use any file that you have the proper codec installed for. Including MP3 and wave files.
  • Play: Plays the music file that you have previously loaded.
  • Pause: Pauses the music. If the music is started again it will start from the same position.
  • Stop: Stops the music.
  • Rewind: Rewinds the music to the very beginning.
  • IsRepeat: Indicates if you have selected repeat mode.
  • Repeat: Allows you to set the repeat mode. If this is activated then the music will loop upon completion. You will need to associate this music player with a window by SetNotifyWindow in order to get the completion events in order for the class to properly repeat the music.
  • SetVolume: Sets the volume level for the current sound. The default model for sounds in DirectSound range from -10000 to 0. I simplified this by making the range go from 0-100%.
  • SetNotifyWindow: Allows you to specify a window that will receive notification events for this music player.

In the future I plan on expanding the class to allow you to handle the events externally from the music class. I also plan on supporting more of the functions for the interfaces that are accessible through DirectShow that will allow you to seek certain positions in a sound track and similar features. Finally I would like to expand this to support a play list. When I expand this it will probably appear as another article on CodeProject.

Bugs

Unfortunately I do not have a wide range of hardware to test this game on, and a large staff to perform all of the testing that I do have. I have found at least one bug that is videocard dependant and I am not sure what the problem is. I have done my best to circumvent the problem. The video card that I experience this problem on is the ATI Rage 128. The game seems to work well in full screen mode, but problems seem to arise when I run in windowed mode on this video card.

There are problems when you run your video card as low as 16-bit color. I have not tested all of the functions at this color depth, but this is something that I plan on inproving in the future.

Occasionally when you start the game by pressing the 's' button, it sticks, and the piece continually repeats to rotate. You can stop this by hitting the 's' key one more time. I will be fixing this by updating the DirectInput code that I have used to DirectX 8.0. When I originally started this I was using all of the DirectX 7 interfaces. I have not had time to update this yet.

Conclusion

This game has taken me about 3 months of full-time work to complete. I started the project to learn DirectX, to learn how to write a game, and to have fun. I did all three! I hope that you are able to learn from my experiences in creating this game also. If all else, I hope you have fun playing it. In addition, I have learned a little bit about copyright law as well.

Credits

I would like to give credit and thanks to my brother Troy for creating all of the graphic images, most of the sound effects, and the completely original soundtrack that is included with the game, thanks Troy.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Paul M Watt
Architect
United States United States
I have been developing software for almost two decades. The majority of my expertise as well as my strongest language is C++ with the networking software as my domain of focus. I have had the opportunity to develop:
* Desktop applications (Data Layer, Business Layer, Presentation Layer)
* Application virtualization
* Web clients
* Mobile device management software
* Device drivers
* Embedded system software for
- IP routers
- ATM routers
- Microwave frequency modems
 
Over the years I have learned to value maintainable design solutions first. This has allowed me to adapt my projects to meet the challenges that inevitably appear during development, including:
* My own misjudgments
* Incomplete requirements
* Feature creep
* Poor decisions for which I have no control
 
I am a Mentor and frequent contributor to CodeProject.com with tutorial articles that teach others about the inner workings of the Windows APIs.
 
I am the creator of an open source project on GitHub called Network Alchemy[^], which is a set of Network APIs that are focused on helping developers write robust network communication software.
 
I maintain my own repository and blog at CodeOfTheDamned.com/[^], a site dedicated to guiding developers through the damn code.
Follow on   Twitter   LinkedIn

Comments and Discussions

 
GeneralVirus Name PinmemberRayBurn012319823-Aug-07 17:00 
GeneralVIRUS PinmemberRayBurn012319823-Aug-07 16:44 
GeneralRe: VIRUS PinmemberPaul Watt7-Aug-07 8:30 
Questionhow to support the network virsus mode Pinmemberhmcentury8-Mar-06 20:31 
AnswerRe: how to support the network virsus mode PinmemberPaul Watt1-Jul-06 8:47 
QuestionCan anybody help me? Pinmembersametkaradag29-Dec-04 0:49 
AnswerRe: Can anybody help me? Pinmembersametkaradag29-Dec-04 11:18 
AnswerRe: Can anybody help me? PinmemberSteve Maier21-Feb-06 3:26 
GeneralBuild instructions for VC .net 2003 PinmemberErik Johnson26-Feb-04 18:57 
Generalseek certain positions in a sound track Pinsussciumegus200324-Feb-04 3:59 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140926.1 | Last Updated 16 Oct 2008
Article Copyright 2002 by Paul M Watt
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid