Creating Screen Saver using OpenGL

You must have seen many beautiful Screen Savers developed in OpenGL. Good examples are '3D Flower Box', '3D Maze' and '3D Text'. Now that we are familiar with OpenGL we too can develop such screen savers.

A screen saver can be installed by right clicking on the screen saver's executable file and selecting the 'Install' option from the menu that appears. As soon as we do this, our screen saver file is entered in the list of screen savers that the Windows OS maintains. This list is displayed in a combo box in a dialog along with its preview. This dialog box, the combo box and the preview window are shown in the following figure. Note that our screen saver titled 'screensaver' also appears in the list.

The screen savers are different than other executables in their extension. All screen savers have an extension of '.scr' rather than the traditional '.exe'. To ensure that it gets this extension, follow these steps:

Select 'Project | Settings | Debug' property page from the 'Developer Studio'. Change the extension from '.exe' file to '.scr' in the edit box with a title 'Executable for debug session' (refer Figure 8-8).

Select 'Project | Settings | Link' property page from the 'Developer Studio'. Change the extension from '.exe' file to '.scr' in the edit box that has a title 'Output file name'. Press OK.

If we click on the 'Preview' button in the dialog box in the first figure the screen saver would be put into action. If we click on another button called 'Settings' one more dialog box would appear which would help the user to control the various elements of the screen saver. Naturally, the appearance and contents of this dialog box would vary from one screen saver to another. The dialog box for our screen saver is shown in the following figure.


As soon as we change the value of time in the dialog box, the change is immediately noticed in the preview window in the dialog box.

How It Works

The screen saver developed here displays a colorful cube having a different color for each of its corners. Moreover, the cube keeps rotating on the screen. This movement is a common feature for most of the OpenGL screen savers. The movement continues till either some key is depressed or the mouse is moved or any button on the mouse is clicked.

The screen saver program runs in three different modes:

  1. Full Screen mode: The Screen Saver runs in this mode in 4 cases:

  1. When the PC is idle for a time greater than one that has been set up through the spin button control in the 'Start | Settings | Control Panel | Display | Screen Saver' property page.
  2. When the 'Preview' button in 'Start | Settings | Control Panel | Display | Screen Saver' property page is clicked.
  3. When we right click on the screen saver file and select 'Test' from the menu that appears.
  4. When we run the screen saver by double clicking the screen saver from the 'Explorer' or from 'Start | Run'.

  1. Small Preview Window mode: The Screen Saver runs in this mode in 2 cases:

  1. When 'Start | Settings | Control Panel | Display | Screen Saver' property page is displayed.
  2. When we right click on '.scr' file and select 'Install'.

  • Settings Dialog Preview mode: The Screen Saver runs in this mode in 3 cases:

  1. When we run the screen saver from the 'Developer Studio'.
  2. When we right click on the screen saver file and select 'Configure' from the menu that appears as shown in Figure 8-10.

  1. When we click on the 'Settings' button from 'Start | Settings | Control Panel | Display | Screen Saver' property page.


The first figure shows the screen saver active in the 'Small Window Preview' mode. The following figure shows it active in the 'Full Screen' mode.


Depending upon which of the above three modes is used to invoke the screen saver it is passed an argument 's', 'p' and 'c' respectively. However, there are two exceptions here. In cases 3(a) and 3(b) above no argument is passed to the program. However, in these two cases too we want the screen saver to run in 'Settings Dialog preview mode'. It is necessary to identify which of these three cases has occurred, because the size of the window where the cube is to appear is different in the three cases.

When the 'Settings' dialog box is popped up, we have made a provision to let the user select the speed of rotation of the cube.

Irrespective of the mode in which our Screen Saver is running, the control would ultimately land in the myapp::InitInstance( ) function. Here it is first determined which command line argument has been passed (if at all) to our program by calling the myapp::checkoption( ) function. It then appropriately calls the function myapp::doconfig( ), myapp::dofullscreen( ), or myapp:: dopreview( ).

The myapp::doconfig( ) Function

If it is determined that the settings dialog box should be popped up, an object of the settings dialog class is created through the statement,

settingdialog d ;

This calls the constructor of the settings dialog. Here the settings dialog is created in memory. This dialog is then displayed by calling CDialog::DoModal( ).

The myapp::dopreview( ) Function

The code of this function is given below:

void myapp::dopreview( )
{

CWnd* parent = CWnd::FromHandle ( ( HWND ) atol ( __argv[2] ) ) ;
CRect r ;
parent -> GetClientRect ( &r ) ;
drawwnd *p = new drawwnd ( TRUE ) ;
p -> create ( NULL, WS_VISIBLE | WS_CHILD, r, parent, NULL ) ;
m_pMainWnd = p ;

}

When this function is called the drawing activity should take place in the preview window. The drawing activity would be managed by the drawwnd class's object. When this object is created the window associated with it should be the child of the preview window present in the property page. Hence the address of this parent window needs to be determined. This has been achieved by calling the function CWnd::FromHandle( ). The argument passed to this function is the handle to the preview window of the property page. This handle is passed to our program as a command line argument (argv[2]). Once the pointer to the parent window has been obtained, the drawwnd::create( ) function is called to create the window.

The myapp::dofullscreen( ) Function

Lastly, if it is determined that the actual screen saver should be put into action, then a full screen window is created. The code of dofullscreen( ) is shown below:

void myapp::dofullscreen( )
{

saverwindow *p = new saverwindow ( TRUE ) ;
p -> create( ) ;
m_pMainWnd = p ;

}

The Settings Dialog

When the DoModal( ) function is called to display the dialog box control first reaches settingdialog::OnInitDialog( ). The code of this function is given below.

BOOL settingdialog::OnInitDialog( )
{

time = AfxGetApp( ) -> GetProfileInt ( "Config", "Time", 0 ) ;
CStatic *s = ( CStatic * ) GetDlgItem ( IDC_PREVIEW ) ;
CRect r ;
s -> GetWindowRect ( &r ) ;
ScreenToClient ( &r ) ;
m_preview.Create ( NULL, WS_VISIBLE | WS_CHILD, r, this, NULL ) ;
return CDialog::OnInitDialog( ) ;

}

In this function, to begin with, the value of time is read from the registry by calling the CWinApp::GetProfileInt( ) function. When you run the program for the first time there won't be any entry in the registry. Hence default value of 0 (last parameter passed to GetProfileInt( )) would be assumed for the time. The value read from the registry (or the assumed value if there is no entry in the registry) is used to show the default selection for speed when the dialog is popped up.

Next, the base class implementation of OnInitDialog( ) is called, which in turn calls settingdialog::DoDataExchange( ). In this function the value of settingdialog::time is used to set up the default value in the edit box. Next, in the OnInitDialog( ) function the size of the preview window is obtained and a preview window is created. Note that the object m_preview (object of drawwnd class) which is a private data member is used to create the preview window. As the window gets created, the drawwnd::OnCreate( ) handler gets called. Here, to set the speed at which the cube should rotate in the window, the CWnd::SetTimer( ) function is called. This function simply sets a timer. Later on we would see how this timer gets serviced.

As we make an entry in the dialog box its result is immediately shown in the preview window. To ensure this a function settingdialog::editchange( ) is called by the framework as soon as we type a new value in the edit box. This function picks up the new value from the edit box using CWnd::GetDlgItemInt( ). This value is then used to reset the timer.

Finally, when the user dismisses the dialog box by clicking OK the time entered by him is written to the registry by calling CWinApp::WriteProfileInt( ) function. The logic of drawing in the preview window has been managed in the drawwnd class.

Drawing In The Window

To manage drawing in the small preview window or the full screen window we have developed a class called drawwnd. Whenever an object of this class is created its zero argument constructor gets called. This constructor function is shown below:

drawwnd::drawwnd ( BOOL deleteflag )
{

m_deleteflag = deleteflag ;
m_time = AfxGetApp( ) -> GetProfileInt ( "Config", "Time", 0 ) ;

}

Here the value of time is read from the registry. Whenever the full screen window or the small preview window is created, control reaches drawwnd::OnCreate( ). Here we have set up a timer by calling CWnd::SetTimer( ).

When the time interval set in CWnd::SetTimer( ) is over the function drawwnd::OnTimer( ) gets called. The code of this function is shown below:

void drawwnd::OnTimer ( UINT id )
{

draw( ) ;
MSG m ;
while ( ::PeekMessage ( &m, m_hWnd, WM_TIMER, WM_TIMER, PM_REMOVE ) ) ;

}

In this function we have called the drawwnd::draw( ) to actually carry out the drawing of the cube.

Full Screen Mode

When the screen saver runs in the full screen mode the myapp::dofullscreen( ) function gets called. In this function we have created an object of saverwindow class which has been derived from the drawwnd class. On creating this object the constructor of saverwindow gets called. In the constructor we have set up the saverwindow::lastpoint variable with a value (-1,-1). This value is later on used in the saverwindow::OnMouseMove( ) function, as we would soon see. Once the object has been created, using it, saverwindow::create( ) function is called. This function in turn calls drawwnd::create( ) with WS_EX_TOPMOST as the first argument and WS_POPUP as one of the values in the second argument. The first argument ensures that the full screen window created becomes the topmost window. The value WS_POPUP ensures that the window created doesn't have a caption bar and the border.

Once the window is created the drawnd::OnCreate( ) gets called which once again sets up a timer in response to which the OnTimer( ) function calls drawwnd::draw( ) to do the drawing in the full screen window.

Disabling The Screen Saver

When the screen saver runs in the full screen mode it should get disabled whenever the user presses any key or performs any mouse operation. It means in the saverwindow class we must handle all keyboard and mouse related messages. Hence we have written the handlers OnKeyDown( ), OnSysKeyDown( ), OnLButtonDown( ), etc. in the saverwindow class. In each of these handlers all that we have done is, sent a WM_CLOSE message to the message queue and then called the base class implementation of the handler. When the WM_CLOSE message would get picked up from the message queue, the application would close itself. The only exception is the OnMouseMove( ) handler. The code of this handler is as shown below:

void saverwindow::OnMouseMove ( UINT flags, CPoint pt )
{

if ( lastpoint == CPoint ( -1, -1 ) )
lastpoint = pt ;
else if ( lastpoint != pt )
PostMessage ( WM_CLOSE ) ;
drawwnd::OnMouseMove ( flags, pt ) ;

}

If you remember, in the constructor of the saverwindow class we have set up the variable saverwindow::lastpoint to a value (-1, -1). In the OnMouseMove( ) handler we have checked whether the coordinates of lastpoint are still (-1, -1). If they are then we do not post the WM_CLOSE message into the message queue. It would be necessary to do so in the following situation. Suppose a WM_MOUSEMOVE message is present in the message queue and the screen saver gets active before the WM_MOUSEMOVE message could get processed. In such an event, as soon as the screen saver becomes active it would get deactivated when the pending WM_MOUSEMOVE message gets processed. Our code given above prevents this by not posting the WM_CLOSE into the message queue in this situation. We also update the value of lastpoint such that next time a WM_MOUSEMOVE message arrives the screen saver is shut down by posting a WM_CLOSE into the message queue.

Destroying The Window

Once the screen saver becomes active we can deactivate it in a number of ways, as discussed above. Even though it has been deactivated from the screen it continues to run in memory. If once again a considerable time elapses without you hitting a key or clicking a mouse yet again it would become active. If it gets activated again then it would yet again reach the myapp::dofullscreen( ) function to create a window. Here the code,

p = new saverwindow ;

would get executed again.

But what has happened to the object that p was pointing last time around. It is still surviving, and now if p starts pointing to another object then a memory leak would occur since we would have no way to access the earlier object. Note that the earlier object would not get destroyed through the destructor of myapp. This is because the destructor doesn't get called since the application is still running and the myapp a object has not gone out of scope.

To avoid this situation we have used a drawwnd::deleteflag variable. While creating the preview window of 'Start | Control Panel | Display | Screen Saver', or the full screen window we have passed a value TRUE to the drawwnd constructor which in turn sets it up in drawwnd::m_deleteflag. As against this, in case of the preview window of the settings dialog we have passed a value FALSE to the drawwnd constructor. This is because on dismissing the dialog box the application would come to an end resulting in call to the myapp class's destructor function. This would destroy the object associated with the preview window using its address which is stored in myapp::m_pMainWnd.

In case of the preview window of 'Start | Control Panel | Display | Screen Saver' and the full screen window the window object would be destroyed in the drawwnd::PostNcDestroy( ) function using the m_deleteflag variable.

No comments: