Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Ramakrishna's Code Project Screen Saver

0.00/5 (No votes)
19 May 2002 1  
This is a screen saver written in C# and Managed C++ to display data from The Code Project web site

Introduction

This is a screen saver written in C# and Managed C++ to display data from The Code Project web site. In this article I would cover basics of designing a screen saver using managed code. The screen saver makes use of different concepts of .NET to provide some really cool features. To install the screen saver:

  1. Download and unzip the zip file containing the executables
  2. Right click on the CPSpirit.scr file and click TestInstall or Configure.
  3. If you wish you may copy all the files into Windows System directory. I don't recommend doing this as later on removal of the screen saver may be slightly difficult

Design Considerations

Following are some important design considerations that went into the design of the screen saver.

  1. Should be fully managed.
  2. Should not consume lot of memory or processor cycles
  3. Should be easily extensible in both the type of data displayed by the screen saver and also the type of animation effects should be easy to add.
  4. Should be easily configuarble by the end user. The end user should be able to control the speed of the animations as well as color and fonts used in the display.

Modes of a Screen Saver

A screen saver is an executable file with an extension .scr which makes window treat it specially. Windows communicates with the screen saver by passing arguments through the command line. A screen saver normally runs in four different modes and the command line specifies what type of mode the screen saver should run in :-

  1. Preview mode. When you see the screen saver settings in the display properties. Windows indicates to the screen saver to run it under preview mode by passing /p switch in the command line followed by the handle of the parent window (as a number) which the screen saver should use to display it's preview.
  2. Configuration Mode. If there are no switches in the command line or if the /c switch is specified the screen saver should show it's configuration dialog with the current foreground window as the owner.
  3. Screen Saver Mode. In this mode the screen saver should run full screen and should normally exit if the user presses a key in the keyboard or uses the mouse.
  4. Password Mode. This mode is for Win9X operating systems. The screen saver should provide a change password dialog to change the password of the user.

My screen saver supports all the three modes except the password mode.

Basic Design of the Screen Saver

Following the principles of object oriented design I came up with different layers for the screen saver code :-

  1. Presentation layer. Given some data it renders the data
  2. Data layer. Maintains the data supports caching, updating and refreshing of the data.
  3. Windowing layer. Provide support for the preview window, configuration and the screen saver window. The preview window and the screen saver window uses the presntation layer for all the rendering. The presentation layer is independent of the windowing layer. It doesnot matter for the presentation layer whether the screen saver is in preview mode or in screen savermode.
  4. Core layer. This layer bring everything else together

The data from the screen saver comes in .cpd files which are XML files. The screen saver loads all the .cpd files in its working(installation) directory. To display custom data just create a .cpd file and copy it in the working directory. The working directory can also be specified in the application configuration file as shown below :-

<configuration>
    <appSettings>
        <add key="WorkingDir" value="G:\wksrc\CPRama\Work" />
    </appSettings>
</configuration>                        

The configuration file for the screen saver would be a file with the nameCPSpirit.scr.configand should be located in the same directory as the CPSpirit.scr file. Note that this is the standard mechanism of application configuration in .NET. If the working directory is not specified in the configuration file or there is no configuration file then the screen saver assumes the installation directory to be the working directory.

.CPD File Schema

Following is a sample .cpd file that is used to display Top10 posters.

<?xml version="1.0" encoding="Windows-1252" ?>
<PresentationData>
  <Title>Top 10 Posters</Title>
  <UpdateAssembly>g:\wksrc\cprama\bin\release\rbwrp.dll</UpdateAssembly>
  <UpdateType>CP.Apps.ScreenSaver.Data.CPTopPosters10Update</UpdateType>
  <LastUpdate>2002-05-16T10:12:14.7663303-04:00</LastUpdate>
  <UpdateInterval>500</UpdateInterval>
  <Items>
    <Item>
      <Text>1. 7828 Messages</Text>
      <Author>Nish </Author>
    </Item>
    <Item>
      <Text>2. 6888 Messages</Text>
      <Author>Christian Graus</Author>
    </Item>
  </Items>
  .
  .
  .
</PresentationData>

Lets look at each individual element in detail

  1. Title. The text which should appear as the title.
  2. UpdateAssembly and UpdateType. UpdateType specifies a type name which implements IPresentationDataUpdater interface defined in ScrMain.dll. UpdateAssembly is the path/name of the assembly which contains the Type.
  3. LastUpdate. The datetime when the data was lats updated
  4. UpdateInterval. The frequency in minutes of updates/refresh of the data.
  5. Items. A list of one or more items 
  6. Item. An item like a lounge message, article etc
  7. Author. An author name for an article or the poster of a message
  8. Text. The text of the item e.g. the message subject or title of the article

How the Data Gets Loaded

.NET framework provides easy means to serialize and deserialize data into XML files. All that needs to be done is to provide attributes in the class and its members. The class which corresponds to the .cpd file is PresentationData and it looks like the following :-

    [XmlRoot]
    public class PresentationData
    {
        .
        .
        .
        [XmlElement]
        public string UpdateType
        {
            get
            {
                return this.updateType;
            }
            set
            {
                this.updateType = value;
            }
        }

        .
        .        
        [XmlArray("Items")]
        [XmlArrayItem("Item")]
        public PresentationItem[] Items
        {
            get
            {
                return this.items;
            }
            set
            {
                this.items = value;
            }
        }
        .
        .
    }        

The attributes specified for each of the public properties indiate how the property is to be written to to XML. In the above the property UpdateType is to be written as the text of XML element UpdateType. This also indicated that each element in array Items should be written as a child element Item of parent elemnt Items. Once the attributes have been specified reading and writing XML files is trivial following code shows how this is done.

public static XmlSerializer serializer = 
                           new XmlSerializer(typeof(PresentationData));

public static PresentationData FromFile(string filePath)
{
    PresentationData pd = null;
    XmlTextReader xtr = null;
            
    try
    {
        xtr = new XmlTextReader(filePath);
        pd = (PresentationData)serializer.Deserialize(xtr);
    }
    catch(Exception e)
    {
        System.Diagnostics.Trace.WriteLine(e.ToString());
    }
    finally
    {
        if (xtr != null)
            xtr.Close();
    }
        
    return pd;
}

The .NET framework XMLSerializer class uses reflection API to generate code at runtime which serializes/deserializes an object of PresentationData based on the attributes specified in the class. I would cover XML serialization in another article if possible.

So when the screen saver starts a background thread is created which loads all the .cpd files into instances of PresentationData.

How the Data Gets Updated

The data in .cpd file needs to be periodically updated. After the data is loaded the screen saver code checks whether the data is outdated or not. Following is the code that checks that :-

public bool NeedsUpdate
{
    get
    {
        if (updateType == null)
            return false;
                
        TimeSpan ts = DateTime.Now.Subtract(lastUpdate);
        return ts.TotalMinutes >= updateInterval;
    }
}

The code is pretty straightforward. If there is no UpdateType specified it means data need not be updated otherwise the time of LastUpdate is compared with the current time to see if it lies within the interval. If it has been determined that the data needs to be updated it is put in a queue called updateQueue. The UpdateNext function shown below uses uses asynchronous programming features provided by .NET framework to update the data.

delegate bool UpdateDelegate();

private void UpdateNext()
{
    Data.PresentationData pd;

    lock(this)
    {
        if (updateQueue.Count == 0)
        {
            isUpdating = false;
            return;
        }
                
        if (isUpdating)
            return;

        isUpdating = true;
        pd = (Data.PresentationData)updateQueue.Peek();
    }
            
    UpdateDelegate delg = new UpdateDelegate(pd.Update);
    delg.BeginInvoke(new AsyncCallback(this.OnItemUpdated), delg);
}

As seen in the code a delegate is created to the Update method of the PresentationData object and BeginInvoke function of the delegate is called. The BeginInvoke method queues the method for asynchronous call in runtime supplied thread pool and executes the Update method on the object on a seaparte thread. The code above also specifies that OnItemUpdated method should be called when the Update method returns. Here is how the OnItemUpdated methods looks like

private void OnItemUpdated(IAsyncResult result)
{
    try
    {
        UpdateDelegate delg = (UpdateDelegate)result.AsyncState;
        bool b = delg.EndInvoke(result);
        Data.PresentationData pd;

        lock(this)
        {
            pd = (Data.PresentationData)updateQueue.Dequeue();
            displayQueue.Enqueue(pd);
        }
                
        if (b)
            pd.Save(pd.FileName);
    }
    catch(Exception e)
    {
        System.Diagnostics.Trace.WriteLine(e.ToString());
    }
            
    UpdateNext();
}

The method basically obtains the delegate on which the asynchronous call was executed and calls EndInvoke that get the return value from the method and throws any exception thrown by the original method. Once the item has been updated it is removed from the updateQueue and is put in a displayQueue. Once the screen saver finishes displaying a particular data it gets the next one and so on.

Finally here is how the Update method looks like

public bool Update()
{
    if (!this.NeedsUpdate)
        return false;
            
    bool ret = true;
            
    try
    {
        Assembly assem = ((updateAssembly == null) || 
                                   updateAssembly.Length == 0) ? 
                                   Assembly.GetExecutingAssembly() : 
                                   Assembly.LoadFrom(updateAssembly);
        IPresentationDataUpdater updater = 
                      (IPresentationDataUpdater)assem.CreateInstance(updateType);

        if (updater == null)
            throw new ApplicationException("Updater could not be created");
            
        updater.UpdatePresentationData(this);
        lastUpdate = DateTime.Now;
    }
    catch(Exception e)
    {
        System.Diagnostics.Trace.WriteLine(e.ToString());
        ret = false;
    }
            
    return ret;
}

The above example shows how to dynamically load an assembly and create an instance of a Type contained in the assembly. By using this mechanism data can be updated from a webservice or though any other means. The example below shows code written in managed C++ that implements the interface and uses WebResourceProvider to update the Top 10 posters list

public __gc class CPTop10PostersUpdate : public IPresentationDataUpdater
{
public:
    bool UpdatePresentationData(PresentationData* data)
    {
        CodeProjectTopPostersProvider topPosters;
        topPosters.fetchResource();
        
        if (topPosters.getFetchStatus() != 0) 
            throw new ApplicationException(
                  new System::String(topPosters.getFetchError()));

        PresentationItem items[] = 
                  new PresentationItem[topPosters.m_vecTopPosters.size()];

        for(UINT i = 0; i < topPosters.m_vecTopPosters.size(); i++)
        {
            CPPoster poster = topPosters.m_vecTopPosters[i];

            items[i].Text = poster.m_strMsgString;
            items[i].Author = poster.m_strName;
        }
                    
        data->UpdateInterval = 500;
        data->UpdateAssembly = this->GetType()->Assembly->Location;
        data->UpdateType = this->GetType()->FullName;
        data->Items = items;

        return true;
    }
};

So by this simple design data can be updated using WebService or through other means shown above. To see how data is updated using the web service refer to ArticlesUpdate and MessageUpdate classes. In fact user can write his own dll and deploy it to support custom means of data update without changing any of the screen saver code.

Animations and Effects

The screen saver should run in preview mode and in full screen mode. Since we want to use the same code for animation effects for both cases, we make both classes implement an interface called IEffectManager defined as follows

    public interface IEffectManager
    {
        event EventHandler NoEffect;
        
        IEffect Effect
        {
            get;
            set;
        }

        void ProgressEffect();
    }

IEffect is an interface that is implemented by classes that provide animations. The ProgressEffect method is called from time to time to progress the animations. Here is how IEffect looks like

public interface IEffect
{
    event EffectCompleteEventHandler EffectComplete;
    void Draw(Graphics g);
    bool Progress();
    void End(Graphics g, IEffectManager em);
}

An effect is an animation like moving of an image, fading in and out of an image, typing text etc. The Draw method draws the current state of the animation. The Progress method advances the animation. The End method gets called when the animation is complete. When the animation is complete the EffectComplete event is fired. This allows for a sequence of effects for example if the image of the Code Project logo comes to a particular point in the screen, Bob image animation starts. This is done by handling the EffectComplete event for the first effect. Here is an example

private void OnCPTextShown(object sender, EffectCompleteEventArgs e)
{
    e.EffectManager.Effect = GetNewBobShowingEffect();
    e.EffectManager.Effect.EffectComplete += 
                     new EffectCompleteEventHandler(this.OnBobShown);
}

The above code is the handler for EffectComplete event that gets fored after the CP logo animation is done. The event handler directs the Effect Manager to a new animation. Finally we require a class for generating sequence of animations. Since there can be many different sequences it is wise to use an interface again called IEffectSequence

    public interface IEffectSequence
    {
        IEffect GetStartingEffect(IEffectManager effMgr, object data, 
                                  object settings);
        
        object DefaultSettings
        {
            get;
        }
    }

Any class implementing IEffectSequence returns a starting effect and handles events from the individual effects to generate a sequence when supplied data for the presentation and the settings. Finally it also provides a method to return default settings for a sequence.

The screen saver uses reflection API to find out all classes implementing IEffectSequence and a special attribute called SequenceAttribute, in all the assemblies in the working directory. The screen saver then randomly circulates between different sequences.

Recent Updates

  • Added support for themes. There was already high degree of control available in manipulating the settings. Now user can save different sets of settings like animation speed, fonts an colors as themes. Play with the configuration dialog to figure this out
  • Added ability to display Nish's Top 10 Weekly posters list.
  • Added quotes of CPians feed.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here