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.
="1.0" ="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
- 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.