A C++ Screen Saver - Part 4

From EDM2
Jump to: navigation, search

Written by Gordon Zeglinski

Introduction

It always seems that no matter how early I start writing the column, something unexpected happens which inevitably makes me miss the submission deadline. After gathering empirical data over the last year or so, I've come to the conclusion that the number of days the column is late is directly proportional to the number of days I start in advance! Fortunately, everyone here tolerates the curse of Murphy^3 that seems to afflict me.

This issue is the last instalment to the experimental DTS screen saver although it's missing some "bells and whistles" (like being able to configure the timeout period w/o recompiling [grin] and not kicking in while in a full screen session), it's functional enough for anyone to easily add in these features. For more info on screen savers see Larry Salomon's article in EDM/2 volume 2 issue 1, "Utilizing Hooks for Added Capabilities."

In this issue, we pick up where we left off in the last issue. Code is added to the handle events from the "Drop module", and "Add module" buttons. Of course to give these buttons any purpose, the module list is now saved and loaded from a file.

Miscellaneous Changes

While working on code to support the "Add Module" button, it became clear that there needed to be some method of determining if the DLL and saver module name (SOM class name) were a valid combination. Instead of explicitly checking for the validity and then creating a SaverEntry, we can use C++ exceptions to abort the constructor if the combination is invalid. The first step is to define a couple of exception classes, as follows.

class SaverEntry{

public:
   class Exceptions{};
   class BogusSaverName:public Exceptions{};
...
};

Figure 1: Exception class for input validation.

When an invalid DLL and saver name combination is specified, the SaverEntry::BogusSaverName exception is thrown. As seen below, if the SOMClassMgrObject->somFindClsInFile call returns NULL, the SaverEntry::BogusSaverName exception is thrown. It's important to note that the destructor for SaverEntry does not get called when an exception is thrown in its constructor. Thus any resources allocated in the constructor that would normally be deallocated in the destructor have to be deallocated before the exception is thrown.

SaverEntry::SaverEntry(const IString &savername,
                       const IString &dllname,
                       SaverModule *mod):
                       SaverName(savername),DLLName(dllname){
   if(mod==NULL){
      somId classId = somIdFromString(SaverName);
      SOMClass *myClass =
         SOMClassMgrObject->somFindClsInFile(classId,
                                             1,
                                             1,
                                             DLLName);

      SOMFree(classId);

      if(myClass==NULL)
         throw BogusSaverName();

      Module=(SaverModule *) myClass->somNew();

      if(Module!=NULL)
         Module->IncCount();
      else
         throw BogusSaverName();
   }
   else
      Module=mod;
}

Figure 2: The constructor for Saver Entry with support for exceptions.

The Configuration File

Reading and writing the config file is pretty simple. The file format will have one saver name and dll name separated by a space per line. To write the file, we use a collection cursor. Instead of using the "forcursor" macro defined in the collection class headers, we use a more conventional C-style for loop.

void SaverMainWin::SaveSetup(){
   ofstream Out("Saver.cfg");  //create an output filestream

   // create a Cursor
   tContainerType::Cursor  iterator(ModuleList);

   // init iterator to first element; loop while iterator
   // points to valid element AND no file output errors;
   // increment iterator to next element
  for(iterator.setToFirst();iterator.isValid() &&
      Out ;iterator.setToNext()){
      //write SaveName DLLName
      Out<<(ModuleList.elementAt(iterator).GetSaverName())
         <<' '
         << (ModuleList.elementAt(iterator).GetDLLName())
         <<endl;
   }
}

Figure 3: Writing the configuration.

When reading the config file, we let the stream classes do most of the work. The stream class will read in the fields using a space or LF as delineations. Thus, we don't have to do any parsing ourselves. The code to load the config file follows:

void SaverMainWin::LoadSetup(){
  ifstream In("Saver.cfg");   //create an input file stream

  if(In){   //if file was opened successfully
     char SaveName[512],SaveDLL[512];

     do{
        // try to read in the savername and saverdll
        In>>SaveName>>SaveDLL;
        // if no error occurred reading add saver to list
        if(In)
           ModuleList.add(SaverEntry(SaveName,SaveDLL) );
     }while(In); //loop until EOF
  }
  else{  //no config file then add in defaults
     ModuleList.add(SaverEntry("TestSaver","TstSave") );
  }
}

Figure 4: Reading the configuration.

The "Add" Button

The "Add Module" button allows new SOM classes to be dynamically registered with the main saver object. To keep things simple (programming-wise), the DLL name is requested separately from the saver name. To get the DLL name, a standard file dialog box is used. Once the DLL has been selected, a dialog box with a single entry field pops up, prompting the user for the name of a SOM class in the DLL. Assuming the user doesn't cancel the operation while it's in progress, an attempt is made to add the given class and DLL combination to the list of saver modules. If the combination is invalid, we catch the SaverEntry::BogusSaverName exception and display a warning message box. The complete event code is presented below. We'll break it up into pieces and see what each does.

case ButAdd:{
   IFileDialog::Settings settings;
   settings.setFileName("*.dll");
   IFileDialog  fd(desktopWindow(),&main,settings);

   if(fd.pressedOK()){
      GetNameWin GetName(11,&main);
      if(GetName.showModally()==1){   //1==DID_OK

         try{
            IEntryField name(NameBox,&GetName);

            main.ModuleList.add(SaverEntry(name.text(),
                                fd.fileName()) );
            main.List.addAsLast(name.text());
            IPushButton(ButDrop,&main.myclient).enable();
         }
         catch(SaverEntry::BogusSaverName arg){
            IMessageBox mb(&main);
            mb.show("Saver Class Not found",
                    IMessageBox::warningIcon |
                    IMessageBox::applicationModal |
                    IMessageBox::okButton |
                    IMessageBox::moveable);
         }
     }
   }
   break;
}

Figure 5: The "Add" button.

We start by creating a file dialog. First we create a settings object. The default settings are fairly reasonable so the only thing we really need to do is set the initial file mask.

IFileDialog::Settings settings;
settings.setFileName("*.dll");

Next we create a file dialog with the desktop window as the parent and the main window as the owner. Note, by default the dialog is application modal.

IFileDialog  fd(desktopWindow(),&main,settings);

If the user pressed OK in the file dialog window, we create a modal dialog window to get the name of the screen saver module (SOM class).

if(fd.pressedOK()){
   GetNameWin GetName(11,&main);
   //1==DID_OK
   if(GetName.showModally()==1){

If the user pressed OK in the name dialog window, we create an instance of IEntryField to get the name entered by the user.

try{
   IEntryField name(NameBox,&GetName);

C++ exceptions allows us to check the validity of the entered names and attempt to add the saver module to the list all in the same line. When the instance of SaverEntry is created, its constructor will throw an exception if the saver name and DLL name are not a valid combination. When the exception is thrown, execution is transferred to the catch block. The SaverEntry is not added to the module list, nor is any additional code executed in the try block.

main.ModuleList.add(SaverEntry(name.text(),fd.fileName()) );

If an exception isn't thrown, the name of the new saver module is added to the list box. The drop push button is enabled.

   main.List.addAsLast(name.text());
   IPushButton(ButDrop,&main.myclient).enable();
}

When the SOM class (saver name) and DLL name combination is invalid, we display a message box to the user.

catch(SaverEntry::BogusSaverName arg){
   //create a message box with the main window as the owner
   IMessageBox mb(&main);
   //display the message box
   mb.show("Saver Class Not found",
           IMessageBox::warningIcon |
           IMessageBox::applicationModal |
           IMessageBox::okButton |
           IMessageBox::moveable);
}

Figure 6: Catching the exception for invalid input.

That's about all there is to the "Add Module" button. Implementing the code for this button has illustrated the following concepts: 1) How to create a standard file dialog. 2) How to manipulate a control within a dialog window. 3) How Exceptions can be used to do error checking in a constructor. 4) How to display a message box.

The "Drop" Button

If we allow users to add modules to the screen saver, we should let them drop them as well. However, we do want to have at least 1 module in the list at all times. As before, the complete source for the drop case is present before it's examined in detail.

case ButDrop:{
   SaverMainWin::tContainerType::Cursor
      iterator(main.ModuleList);

   if(main.ModuleList.locateElementWithKey(
         main.List.itemText(main.List.selection()),
         iterator)) {
      main.ModuleList.removeAt(iterator);
      main.List.remove(main.List.selection());
      if(main.ModuleList.numberOfElements()<=1)
         IPushButton(ButDrop,&main.myclient).disable();
   }
   break;
}

Figure 7: The "Drop" button.

The drop button will remove the currently selected item in the list box from the list box and the module list. To remove the item from the module list, the name of the saver module (which is the name of the SOM class) will be used. Let's for the moment assume that there is a possibility that the saver name returned from the list box is not in the module list. In order to avoid having an exception throw by the collection classes, we query the ModuleList for the elements position that corresponds to the given key. The query returns true and points the passed iterator to the proper element.

SaverMainWin::tContainerType::Cursor
   iterator(main.ModuleList);

if(main.ModuleList.locateElementWithKey(
   main.List.itemText(main.List.selection()),
   iterator)) {

At this point, the iterator is pointing to the element we want to drop from ModuleList. So we remove the element pointed to by iterator from module list and drop the current selection from the list box.

      main.ModuleList.removeAt(iterator);
      main.List.remove(main.List.selection());

It doesn't make sense to leave the drop button enabled if there only 1 entry in the list. So we disable the button if there is 1 or less entries.

      if(main.ModuleList.numberOfElements()<=1)
         IPushButton(ButDrop,&main.myclient).disable();

That's about it for the drop button. It's more efficient to query a key based collection for the elements position than it is to ask for the element directly (or remove the element directly) if the existence of the elements key in the collection is not guaranteed. The overhead of throwing an exception for a non-existent key is greater than the previous method.

Getting User and Timer Input

The basic principle behind a screen saver is that it becomes active after a given amount of time has elapsed in which no user activity has occurred. To implement this, we need to use an input hook (see EDM/2 volume 2 issue 1) and a timer event. To implement an input hook, we have to drop to the C API, create a DLL and have the screen savers main window handle specific PM message types. I'll skip most of the DLL details and summarize its interaction with the screen saver as follows. The screen saver initializes the DLL on startup, causing an input hook to be registered with OS/2. Each time user input happens, the input hook sends the main window of the screen saver a WM_STOP_SAVER message. Initially I was planing on putting more logic in the input hook, but at a later time decided not to. Thus, the name WM_STOP_SAVER arose.

To handle the non-standard message types, we have to create a custom event handler. The code to do this follows.

class SaverHandler:public IHandler{

public:
   SaverHandler(SaverMainWin &mw);
   ~SaverHandler();

protected:
   Boolean dispatchHandlerEvent(IEvent &event);

   SaverMainWin &mainwin;
};

Figure 8: The SaverHandler class.

As seen above, we subclass IHandler and override the dispatchHandlerEvent member function. Because this is an application specific handler, a few steps have been combined into one. As seen below, the constructor takes a reference to an instance of the savers main window. It uses this reference to automatically add itself as an event handler for that window and store the reference.

SaverHandler::SaverHandler(SaverMainWin &mw):mainwin(mw){
   handleEventsFor(&mainwin);
}

Figure 9: Installing the SaverHandler object.

Typically the dispatchHandlerEvent member function simply checks the eventId. If it recognizes the eventId, it creates a specific IEvent type and calls another virtual member function. This way, the application can provide specific behavior by overloading the later member function. In this case we'll skip the introduction of virtual member function and implement the application specific handling directly in dispatchHandlerEvent. If the eventId is not recognized, false is returned so that other registered event handlers may attempt to process the event. The code for dispatchHandlerEvent follows.

Boolean SaverHandler::dispatchHandlerEvent(IEvent &event){

  switch(event.eventId()){
     case WM_START_SAVER:
     case WM_STOP_SAVER:
        mainwin.GotInput();
        break;
     default:
        return false;
  }
return true;
}

Figure 10: The SaverHandler::dispatchHandlerEvent() method.

In OS/2, the ITimer class is implemented more like the IThread class than an event handler class. It is based on WM_TIMER messages. When the time interval expires, the callback function is called. In the SaverMainWin class (for full class definition see MainWin.hpp), the timer related definitions are:

//declare the timer
ITimer myTimer;

//declare the callback function thunk
ITimerMemberFn0 myTimerFunc;

In the constructor for SaverMainWin (see MainWin.cpp for full source), the timer is started by the line:

myTimer.start(IReference(&myTimerFunc),
              1000); //use 1 second intervals

The timer callback function shown below checks the current time. Recall, in PM a WM_TIMER message may not be processed exactly at the specified time interval. The call to time() gets the current time to compensate for this. If the difference between the current time and the last time user input was observed is greater than 1 minute, the currently selected screen saver module is activated.

void SaverMainWin::TimerFunc(){
  CurTime=time(NULL);

  if(!IsSaving && difftime(CurTime,LastInTime)>60){
     StartCurSaverModule();
  }
}

Figure 11: The inactivity timer code.

Changes to Saver SOM Objects

The SaverModule object now has two static member functions and one static data member. These static members allow instances of SaverModule descendents to interact with the main window. Specifically, when an active instance of a SaverModule descendent wants to stop "saving the screen", it calls the static function SaverModule::SignalStop().

The TestSaver class when actively saving the screen, creates a window that covers the whole desktop and paints this window black. There are two ways to determine when an active saver module should be deactivated. First, the hook DLL could be used to monitor keyboard/mouse input. Second, the active screen saver window could monitor the keyboard and mouse input and dismiss itself when any input has been noticed. The second approach has been taken here. To receive the keyboard input, the saver window set the focus to itself. Also, note that the test saver will only deactivate itself when a key is pressed.

Wrapping Things Up

This concludes the screen saver series. Although the screen saver developed isn't very elegant, it has served its purpose. That is, it was a good sample application to explore DTS, IBM's collection classes and IBM's user interface library. Anyone wishing to develop the saver further is free to do so.