Ramakrishna's Code Project Screen Saver






4.29/5 (4 votes)
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:
- Download and unzip the zip file containing the executables
- Right click on the CPSpirit.scr file and click Test, Install or Configure.
- 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.
- Should be fully managed.
- Should not consume lot of memory or processor cycles
- 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.
- 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 :-
- 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.
- 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.
- 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.
- 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 :-
- Presentation layer. Given some data it renders the data
- Data layer. Maintains the data supports caching, updating and refreshing of the data.
- 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.
- 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.config
and 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 [BusterBoy]</Author>
</Item>
<Item>
<Text>2. 6888 Messages</Text>
<Author>Christian Graus</Author>
</Item>
</Items>
.
.
.
</PresentationData>
Lets look at each individual element in detail
- Title. The text which should appear as the title.
- 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. - LastUpdate. The datetime when the data was lats updated
- UpdateInterval. The frequency in minutes of updates/refresh of the data.
- Items. A list of one or more items
- Item. An item like a lounge message, article etc
- Author. An author name for an article or the poster of a message
- 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.