Introduction
I wanted to write a screensaver. Yeah, I bet everyone goes through this stage, but this time I got the needed info so the purpose of this article is to impart to you the info that for so many years was missing or hidden or only available to a privileged few.
First of all I would like to thank Rakesh Rajan for his fine article that got the ball rolling, and I was able to fill in the gaps and complete a screensaver with all the features I really could possibly want. You can view his fine article - How to develop a screen saver in C#.
Hmmm, that's a long URL, I hope it fits! Thanks again!. Further credits to two others can be found in the source code.
Double-buffering
This is a technique to do flicker free, and faster drawing. It involves doing all your drawing on an off-screen bitmap. Because this bitmap is not on display the drawing routines run much faster because the drawing doesn't invoke a paint event. After all drawing is done, it is processed for display. There are extremely quick double-buffering that forms and other controls have built in, but it has its issues, which means you might be tortured by the InvalidOperationException
error. This was a particularly nasty error to try to deal with because in my program, it drops right through the catch
statements that I made for this error as if it did not match and what makes it worse is it's so intermittent, it might show up immediately, or might run for several hours before showing its ugly head. To rid yourself of this nefarious bug, use the Flush
method on your Graphics
object after you've done a drawing cycle . Example ... pev.Graphics.Flush(System.Drawing.Drawing2D.FlushIntention.Flush);
and this will eliminate the error. The cause of the error is that unless you do use the above command, sometimes, the buffer is not drawn to the screen right and your code could end up doing another paint cycle while the buffer is being written, or usually while the buffer is being disposed, which causes a conflict where your code is trying to use the buffer, while it is unavailable! The GDI+ does not check for this. If you use the Flush
as above, the buffer is written to the screen and refreshed and ready to use when you need it.
Update: 06/18/2005 - I have solved the above issue. It is quite an interesting find, as to why. See end of article for more info.
6/10/2005- Neat new stuff added. You gotta try this with seven swarms a screen!
I have just succeeded in getting this screensaver to work on multiple monitors in duel mode on a single system at the same time!! I am going to upload the new code, and then I'll work on the snippets, for that is quite a job. As far as I know this is the only screensaver that can do this. I tried many others, and none worked. I have explained what I had to do in EntryPoint.cs in the source files. I have been having some stability issues. Hopefully the changes I made on 6/13/2005 fix some of them.
6/16/2005- I have implemented event and delegate driven engine. I separated some of the code that was getting rather cluttered into separate classes, solved the stability issues and modified snippets.
See end of article!
After searching the internet for weeks, trying to find the info I needed to make my own screensavers and coming up with woefully incomplete examples that glossed over vital information, or examples that were enormous, yet devoid of meaningful comments so that by the time I found the answer, I had forgotten the question. Almost all the examples did not work or had bugs that prevented me from testing them. I determined that I would write a "Complete" screensaver and post it on the web so that others need not go through the torment I did trying to find the info needed. What I mean by putting "Complete" in quotes, is that the only feature I did not implement was bringing up the password dialog box when the program received an/a as an argument. I did not wish to have some bug or future change in Windows, to cause my screensaver to lock a user out of the system. Caveat: On NT 2000, if the computer has the feature to lock the computer turned on after X minutes of inactivity, and the computer is locked while the screensaver is running, when you touch your machine, the screensaver shuts down, but the welcome box from Windows asking for the password does not show. The fix is simple .. Hit Ctrl-Alt-Delete or Ctrl-C and the welcome screen comes up and you are good to go.
This article will still give you the info you need to make your own screensaver.
The problems I needed to solve were ....
- How to attach the Configuration dialog as a child of the Display Properties box from Windows.
- How to draw to the little mini-preview box on the above form.
- Where to put the drawing routines and how should I execute them.
- How to save my configurations to the registry, and just recently, how to save CONFIG stuff to an XML file instead of the registry.
- How to get a screensaver instance to draw to all active screens on a system!
Using the code
For your own use just replace clsInsects
with your own class of drawing routines, and then modify the load
event and the paint
event in the main ScreenSaverForm.cs and the code in EntryPoint.cs, ScreenSaver.cs, MiniPreview.cs, XmlScreenSaverConfig.cs and the ConfigForm.cs to your liking and have fun!
This project used to be separated into a couple DLLs and an EXE. Now there are no DLLs, you just compile, rename the EXE as .scr and copy to the system32 directory and then choose the name as your screensaver and you are done or you can configure some nice options.
The code in StructsAndFunctions.cs could be used for your own programs, it does not depend on any of the other modules, but the Insects
class and the other modules all depend on StructsAndFunctions.cs.
The ScreenSaverForm
load
event initializes the size of the form and my Insects
' class instance which do all the drawing. Then a while
loop in the EntryPoint
classes Main
method calls each form using an appropriate method to invoke the screens to draw and show the results.
Here is how I got the program to work with multiple monitors. The code is in Entrypoint.cs under the "/s" case
statement.
I create an array of ScreenSaverForm
s which handles the full-screen drawing called sf
. Then I go through the array with a while
statement and call the show method of each ScreenSaverForm
in the array, I then call the Application.DoEvents()
when I get a signal from manualEvents
that a delegate has been called by a form with drawings that are ready for display. I found that I had to cause the forms to draw the graphics using the Refresh()
method on the forms and then call Application.DoEvents()
to cause the drawing to appear on multiple monitors.
- If the system only has one monitor, I found that calling
sf[i].Invalidate(sf[i].Bounds)
method where i
= the screen number, the primary screen worked fine and was faster when this method failed to cause the second monitor to show anything. The first monitor would hog all the CPU time at the complete expense of the second.
- I found that if I used the
ScreenSaverForm
's Refresh()
method when there were multiple active screens, I was able to invoke drawing on both monitors and the results would show when I called Application.DoEvents()
. I no longer use Invalidate
as it caused synchronization issues with timing as I learned that events are on a separate thread, and Refresh()
was the only way to get the truth, of the state of a variable I used to query if the form was ready for another call to PaintMe()
.
- I have my code for drawing in the screensaver forms one form per monitor, but I had issues with the program triggering the form to draw before it was finished and ready to draw again.
Update: I ended up writing some delegates and passing them into the forms and using these delegates to notify the calling class ScreenSaver
when a form was ready for drawing via Application.DoEvents()
and then invoking a new paint
event via the form's Refresh()
method.
Originally, I had written this to use threading, but I found that the worker threads were very slow compared to having all instances of the ScreenSaverForm
running under the main thread. It was also good for avoiding thread locking issues. With one thread acting as if it were multiple threads, there was no chance of thread locking problems. Now, I do the drawing in the paint
events, wherever possible.
One thing you will notice right off is that the source code is chock full of comments to the point that the code itself is nearly buried for two reasons...
- It was better than searching through mounds of incomprehensible code, only to discover that the feature I'm trying to emulate, isn't even in the sample at all or the sample is so broken, I can't get it to work, or finding that the code is so difficult to understand, that I can't implement or find the solution.
- The code contains a special type of comment that starts with /// that contains tags that are used by the IDE to generate a local web page where the user can look up info on virtually every object in the code by clicking on links and get a good idea of what the code is all about. Within a folder created by extracting this example you will find a folder called CodeCommentReport that contains Solution_SwarmScreenSaver.HTM. This is the file you should open with your browser to see this neat feature. Also see GettingStarted.txt for info on installing the screensaver.
Most of the comments is for reason #2, but I wanted to make sure the code was understood. I try to make few assumptions that the reader knows something.
Problems that had to be solved ...
- Code comment HTML was not working correctly. To find the fix go to this link or follow the procedure outlined below ...
Build Comment Web Pages in VS 2003 and Windows XP SP2. If you use the documentation created by Build Comment Web Pages in Visual Studio 2003, you will experience problems viewing the documentation after upgrading to Windows XP SP2. Currently, you can download the release candidate for testing purposes here: The service pack includes numerous fixes to improve the overall security of Windows and Internet Explorer. Unfortunately, one fix disallowed a tag that was generated by the Build Comment Web Pages feature.
Workarounds
The easiest workaround is to use Visual Studio to do a Find and Replace to change the tag in the documentation to one that is accepted by Internet Explorer.
- Go to Edit/Find and Replace/Replace in Files.
- In the Find what: type: <!-- saved from url=(0007)http:// -->
- In the Replace with type: <!-- saved from url=(0014)about:internet -->
- Ensure Regular Expressions is not checked.
- In the look in type the directory where the web pages are stored.
- Click Replace All to execute the search.
Background
The tag is that the problem here is called the Mark of the Web. IE adds the Mark of the Web to web pages that are saved from the internet. This way IE can detect that the content was originally from the internet zone and it should be treated as such. In this instance, the page was not saved from the internet but Visual Studio used http:// to force it to the internet zone. In Windows XP SP2, the IE team did work to tighten up the items which were permissible and http:// was accidentally excluded. The IE team assures me that the original mark of the web that is generated by VS 2003 does not represent a security risk.
If you have any questions, please let me know.
-Sean Laberee
Posted on Friday, July 16th, 2004 5:07 PM by VSEditor.
Important namespaces classes and filenames....
Namespace CommonCode
contains structure definitions that are used in various projects in the solution. StructsAndFunctions.cs contains these structs and a class called CommonFunctions
that contains functions random number generation and color adjustment routines so the swarm isn't too dark to be visible. These are used by the drawing routines in clsInsects
. The color adjustment routines are not totally accurate according to the site, but it helped a lot. Thanks to George Shepherd's site.
XmlSwarmScreenSaverConfig.cs contains a self-recovering XML CONFIG reader/writer that is used to save your screensaver settings. The saver tries to read the configuration which is named like SaverName.scr.config.xml. You must rename the resulting .exe extension to .scr and copy the two DLLs that result from compiling this source code into the System32 directory.
If the reader fails to find the file, or finds the structure of the file varies because I added a feature, it creates a new one containing default values. This makes configuration saving extremely transparent and easy to utilize. Feel free to use this module for saving configuration info in your own apps.
Namespace SwarmScreenSaver
contains the EntryPoint
class that is where the program begins execution, and the definitions of the CONFIG and the ScreenSaverForm
which is the form used to display drawing in full-screen mode. There is also the MiniPreview
class that handles drawing to the mini-preview window. The ScreenSaver
class is used to run the screensaver in full-screen mode and handles shutting down of the app when done.
Namespace Insects
contains my code for the clsInsects
that is the class that does the drawing to the graphics object it receives.
- How to make a window of my application a child of the Windows screensaver dialog box by using the number passed in when Windows indicated the user wanted to configure the screensaver.
When the user clicks the settings button Windows starts up the program with a commandline like this ... /c:######## where # is supposed to be a digit in a 10 base number that is the handle for the dialog box shown above. I've modified this so that the user can type swarm /c to bring up the CONFIG box.
The reason this was desired was if the user closed down the dialog box, my program was supposed to close down too. Setting up this Child-Parent relationship proved impossible in C# because newer versions of windows implements security that does not allow such relationships unless both Windows were within the same application! To solve this issue for the CONFIG form was to drop a timer control on the form and set it to check for the visibility of the Display Properties window every tenth of a second and close if it was gone.
The API calls used below reside in a class in StructsAndFunctions
called by the rather unimaginative name of CommonFunctions
. First I declare the API calls I need and then I declare wrappers for them, because they could not be called directly but only via these wrappers.
Watch out!!, here comes the code!
Declares an API call to determine if a window is visible. hWnd
is a handle to the window being checked for visibility. Returns true
, if the window is visible, else false
.
[DllImport("user32.DLL",EntryPoint="IsWindowVisible")]
private static extern bool IsWindowVisible(IntPtr hWnd);
Declare the API call to get the size and location of the client window.
hWnd
is a handle to the window being addressed. Struct containing Size and Location to be filled with the client area size and location as pixels in RECT rect
instance passed in. Returns true
if success, otherwise false
.
[DllImport("user32.dll")]
private static extern bool GetClientRect(IntPtr hWnd, ref RECT rect);
Wrapper to call the IsWindowVisible
API call. hWnd
= Handle to the Desktop Properties window. Returns true
if visible, else false
.
public bool IsWindowVisibleApi(IntPtr hWnd)
{
return IsWindowVisible(hWnd);
}
Wrapper to call GetClientRect
API functions in user32.dll to get size of the client area of a window. hWnd
is a handle to the desktop properties dialog box. rect
is an instance of a RECT
struct
to be filled with the location and size of the client area. Returns true
if successful, false
if unsuccessful.
public bool GetClientRectApi(IntPtr hWnd, ref RECT rect)
{
return GetClientRect(hWnd, ref rect);
}
Here is a sample snippet from my CONFIG form.
namespace SwarmScreenSaver
{
public class ConfigForm : System.Windows.Forms.Form
{
private static IntPtr cParentWindowHandle =
new IntPtr(0);
CommonCode.CommonFunctions cf = new CommonFunctions ();
Here is the constructor. Insert the code below the InitializeComponants
call in the constructor of your configuration form. I modified this code below to work when CONFIG is brought up by the running swarm with /c as a command line.
In the configure mode, the handle starts at the third character - Example: /c:345678.
public ConfigForm(int IntArg)
{
InitializeComponent();
***** Start of the code you'll need to insert.
If no handle, skip this. User started CONFIG using /c on command line. I use a parsing routine in the EntryPoint
class to get the handle as a number that is passed to the rest of the program as needed.
if (IntArg != 0)
{
cParentWindowHandle = (IntPtr) IntArg;
CheckToCloseTimer.Enabled = true;
}
Here is what handles closing the CONFIG form when needed....
private void CheckToCloseTimer_Tick(object sender,
System.EventArgs e)
{
if (cf.IsWindowVisible(cParentWindowHandle) == false)
{
CheckToCloseTimer.Enabled = false;
Close();
}
}
}
}
That is all you need for controlling when to close the CONFIG form in response to the Display Properties box closing. Remember to look closely at the methods in the code for retrieving, verifying and saving configuration data.
Caveat: I wanted to actually track the location of the Windows Display Properties dialog box, but when I moved it, the above code would think it was invisible and close my CONFIG window.
- When the screensaver received /p:####### this meant that my screensaver was to draw to the small mini-preview window in the Display Properties dialog. We need to get the size of the little black box we draw the mini-preview in. To do this we need a
RECT
structure to pass in.
Here is my definition of a Rectangle from the CommonCode
namespace. Notice!! This is not really in this file but is elsewhere and is shown here for clarity. This RECT
definition is in StructsAndFunctions
class. rect
is sent to the API call GetClientRect
via GetClientRectApi
to get the size of the mini-preview window in pixels....
public struct RECT
{
public int left;
public int top;
public int right;
public int bottom;
public RECT(int l, int t, int r, int b)
{
left = l;
top = t;
right = r;
bottom = b;
}
}
Here is the start of code for the EntryPoint
class.
using CommonCode.XmlConfig;
namespace SwarmScreenSaver {
[STAThread] before the Main is no longer needed since the new 1.1 framework.
public class Entrypoint
{
private static void ParseArgsToPrefixAndArgInt(string[] args,
out string argPrefix, out int argHandle)
{
string curArg;
char[] SpacesOrColons = {' ', ':'};
switch(args.Length)
{
case 0:
argPrefix = "/s";
argHandle = 0;
break;
case 1:
curArg = args[0];
argPrefix = curArg.Substring(0,2);
curArg = curArg.Replace(argPrefix,"");
curArg = curArg.Trim(SpacesOrColons);
argHandle = curArg == "" ? 0 : int.Parse(curArg);
break;
case 2:
argPrefix = args[0].Substring(0,2);
argHandle = int.Parse(args[1].ToString());
break;
default:
argHandle = 0;
argPrefix = "";
break;
}
}
This is the starting point for the program. It gets or creates the configuration info on a read attempt, from swarm.scr.config.xml and contains a case
statement that handles the various command line states and requests.
static void Main(string[] args) {
int baseVelocity;
int beeCount;
int colorCycleSeconds;
bool glitterOn;
int swarmsPerScreen;
string argPrefix;
int argHandle;
XmlConfigSaver xm =
new CommonCode.XmlConfig.XmlConfigSaver();
xm.readConfigXml(out baseVelocity, out beeCount,
out colorCycleSeconds, out glitterOn,
out swarmsPerScreen);
xm = null;
if (args.Length > 2)
{
MessageBox.Show("Too many arguments " +
"on the command line.");
return;
}
ParseArgsToPrefixAndArgInt(args, out argPrefix,
out argHandle);
switch (argPrefix)
{
case "/p":
if (argHandle == 0) goto case "/s";
else
{
MiniPreview mpTemp = new MiniPreview();
mpTemp.DoMiniPreview(argHandle, baseVelocity,
beeCount, colorCycleSeconds,glitterOn);
mpTemp = null;
}
break;
- The Display Properties dialog box would appear to be invisible for a number of seconds before a call to
IsWindowVisible
API would return true
. I had to write in a loop that would wait up to 30 seconds for the window to show. I could probably get by with only 4 seconds, but it doesn't matter, I don't have to wait too long. Here is the code for solving that problem. As in the CONFIG form, put this code in your .cs file that contains your static void Main(String[] args)
method call. In my case, my class is EntryPoint
in EntryPoint.cs. Here is the code below, the code addressing the solution to this is encased in Asterisks. Look for the italics below in the next code section.
Here is the code within the MiniPreview
class ...
using System;
using CommonCode;
using System.Windows.Forms;
using System.Drawing;
using Insects;
using System.Threading;
namespace SwarmScreenSaver
{
public class MiniPreview
{
public void DoMiniPreview(int argHandle,
int baseVelocity, int beeCount,
int colorCycleSeconds, bool glitterOn)
{
IntPtr ParentWindowHandle = new IntPtr(0);
ParentWindowHandle = (IntPtr) argHandle;
RECT rect = new RECT();
using(Graphics PreviewGraphic =
Graphics.FromHwnd(ParentWindowHandle))
{
CommonFunctions cf = new CommonFunctions();
cf.GetClientRectApi(ParentWindowHandle,
ref rect);
DateTime dt30Seconds =
DateTime.Now.AddSeconds(30);
while (cf.IsWindowVisibleApi(
ParentWindowHandle) == false)
{
if (DateTime.Now > dt30Seconds) return;
Application.DoEvents();
}
Bitmap OffScreenBitmap =
new Bitmap(rect.right - rect.left,
rect.bottom - rect.top, PreviewGraphic);
Graphics OffScreenBitmapGraphic =
Graphics.FromImage(OffScreenBitmap);
clsInsects insects =
new clsInsects(baseVelocity,
beeCount, colorCycleSeconds,glitterOn);
insects.initSwarm(3, rect.right - rect.left,
rect.bottom - rect.top);
while (cf.IsWindowVisibleApi(
ParentWindowHandle) == true)
{
OffScreenBitmapGraphic.Clear(Color.Black);
insects.DrawWaspThenSwarm(
OffScreenBitmapGraphic);
Thread.Sleep(50);
try
{
PreviewGraphic.DrawImage(OffScreenBitmap,
0,0,OffScreenBitmap.Width,
OffScreenBitmap.Height);
}
catch
{
break;
}
Application.DoEvents();
}
insects.StopNow = true;
cf = null;
OffScreenBitmap.Dispose();
OffScreenBitmapGraphic.Dispose();
PreviewGraphic.Dispose();
}
}
}
}
Here is the code for handling the full screen screensaver mode in EntryPoint
...
case "/s":
ScreenSaver screenSaver = new ScreenSaver();
screenSaver.RunMeTillShutdown( baseVelocity,
beeCount, colorCycleSeconds,
glitterOn, swarmsPerScreen);
screenSaver = null;
break;
default:
break;
}
}
Now, I just use this catch
below to display any unhandled exceptions:
catch(System.Exception e)
{
MessageBox.Show("ScreenSaver:" + e.Message +
" Src:" + e.Source.ToString());
}
Now here is the code within class ScreenSaver
that handles the full screen mode. Pay special attention to the areas in bold. I will be addressing these at the end of the article under updates:
using System;
using System.Windows.Forms;
using System.Diagnostics;
namespace SwarmScreenSaver
{
public delegate void DonePaintingDelegate(int screenNumber);
public delegate void ShutDownDelegate();
public class ScreenSaver
{
public DonePaintingDelegate DonePaintingDel;
public ShutDownDelegate ShutDownDel;
int screenCount;
bool shuttingDown = false;
ScreenSaverForm [] sf;
ManualResetEvent[] manualEvents;
public ScreenSaver()
{
manualEvents = new
ManualResetEvent[Screen.AllScreens.Length+1];
for (int i = 0; i <= Screen.AllScreens.Length; i++)
{
manualEvents[i] = new ManualResetEvent(false);
}
}
public void DonePainting(int screenNumber)
{
lock(ScreenDonePainting)
{
ScreenDonePainting[screenNumber] = true;
manualEvents[screenNumber].Set();
}
}
public void ShutDown()
{
lock(manualEvents)
{
shuttingDown = true;
manualEvents[Screen.AllScreens.Length].Set();
}
}
public void RunMeTillShutdown(int baseVelocity,
int beeCount, int colorCycleSeconds,
bool glitterOn, int swarmsPerScreen)
{
DonePaintingDel =
new DonePaintingDelegate(DonePainting);
ShutDownDel = new ShutDownDelegate(ShutDown);
screenCount = Screen.AllScreens.Length;
sf = new ScreenSaverForm[screenCount];
int i = 0;
for (i = 0; i < screenCount;i++)
{
sf[i] = new ScreenSaverForm(i,DonePaintingDel,
ShutDownDel, baseVelocity,
beeCount, colorCycleSeconds,
glitterOn, swarmsPerScreen);
sf[i].Show();
sf[i].Refresh();
}
while (screenCount > 0)
{
WaitHandle.WaitAny(manualEvents);
if (shuttingDown == true)
{
for (i = 0; i < Screen.AllScreens.Length; i++)
{
if ((sf[i].Visible == true))
{
sf[i].CloseMe();
screenCount = 0;
Application.DoEvents();
}
}
continue;
}
else
{
try
{
for (i = 0; i < Screen.AllScreens.Length; i++)
{
if (ScreenDonePainting[i])
{
manualEvents[i].Reset();
Application.DoEvents();
sf[i].Refresh();
}
}
}
catch( InvalidOperationException e)
{
EventLog.WriteEntry("SwarmScreenSaver" +
e.Source, e.Message,
EventLogEntryType.Error);
}
}
}
}
}
}
- The random color chosen was often too dark to see against a black background.
The code for adjusting the brightness of the color is in the CommonFunctions
class in the StructsAndFunctions.cs file. It is huge and this article is getting too big and unwieldy as it is, with me jumping back and forth, when I remember or if I remember I've forgotten something vital!
- Stutter and flicker. I solved this via double-buffering and moving the drawing calls to the
paint
event.
Remember that I have my drawing routines in the ScreenSaverForm_Paint
event, but I invoke this from outside the form in a while
loop in ScreenSaver.RunTillShutdown()
method calling ScreenSaverForm.Refresh()
and then Application.DoEvents()
to get the results to show on the screens when one of the elements in manualEvents
is set, indicating that the form is done painting.
The original code had a lot of short
s to represent points on the screen, and this makes sense, so I tried to save memory by using short
s, but all the random number generation routines available to me, returned int
s, and all the explicit conversion I had to do, slowed the program down, so I got rid of the short
s and made everything int
s.
Here is the relevant snippets in the ScreenSaverForm.cs ....
This goes inside the declaration of the class but before the constructor ... Contains the mouse position. If it moves, all instances of ScreenSaverForm
are closed and the app is ended.
private Point MouseXY;
private int ScreenNumber;
private int SwarmCount;
clsInsects[] insects;
DonePaintingDelegate DonePaintingDel;
ShutDownDelegate ShuttingDownDel;
private bool stopNow = false;
The paint
event calls the drawing routines as long as stopNow
is false
and then calls Invalidate
which in turn evokes the paint
event again until time to quit. No timer is needed.
Constructor for this form, initializes any components:
public ScreenSaverForm(int scrn,
int baseVelocity, int beeCount,
int colorCycleSeconds,
bool glitterOn, int swarmCount)
{
InitializeComponent();
SwarmCount = swarmCount;
DonePaintingDel = donePaintingDel;
ShuttingDownDel = shutDownDel;
insects = new clsInsects [SwarmCount];
for (int i = 0; i < SwarmCount; i++)
insects[i] = new clsInsects(baseVelocity, beeCount,
colorCycleSeconds, glitterOn);
ScreenNumber = scrn;
The stuff below tells the form that it will be doing all drawing in the paint
event and to use it's own double-buffering routines built in, UserPaint says the FORM code will do the repainting rather than having the OS decide when to do so and Opaque says we will control when to redraw the background which since it is always set to black with double-buffering, this never needs to be done. This is quite an interesting piece of code. By specifying all these variables including DoubleBuffer
we are telling the form to use its own special Win32 low level bitmap routines that are extremely fast. There is a side effect to this though. This special buffer used by the form has all bits set to 0 which is a black background and contains nothing from any previous calls to the drawing routines. This has the side-effect of causing any drawing that happened in the previous call to the drawing routines to be done away with. Consequently, you will notice that a lot of code involved in erasing previous positions is commented out because there is nothing to erase. All your drawing routines need to do is draw and don't worry about trying to erase stuff like lines by drawing a black line over it, it is all taken care of for You. The caveat is that I had an option to cause the swarm objects to leave a trail. This was interesting, but the speed increase I got over using the form's built in superior double buffering was just too good to pass up despite the side-effect. I might figure out a way to fix this anyway. I'll look into this later.
In the code below, all the enum
s below represent ONE but in a 32 bit int
. The enum
s are OR'd with the others, and then finally with the value true
, which has the net effect of setting all the bits that these values represent to 1.
SetStyle
(
ControlStyles.AllPaintingInWmPaint |
ControlStyles.UserPaint |
ControlStyles.DoubleBuffer |
ControlStyles.Opaque, true); } }
Here is the load
event handler, it initializes my buffers and the Insects
crawling class and then calls invalidate
, which starts the paint
event, that does the drawing then calls itself. What it calls itself, I do not know. Hehe, sorry, I love silly humor.
private void ScreenSaverForm_Load(object sender, System.EventArgs e)
{
Bounds = Screen.AllScreens[ScreenNumber].Bounds;
Cursor.Hide();
TopMost = true;
for (int i = 0; i < SwarmCount; i++)
insects[i].initSwarm( 50, Bounds.Width, Bounds.Height);
}
Notice the disposal of our Graphic
and Bitmap
objects below ...
protected override void Dispose( bool disposing )
{
if( disposing )
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}
The ScreenSaver.RunMeTillShutdown
method uses a while
loop to call Application.DoEvents()
which causes the screens to show on the monitors when a ScreenSaverForm
is done painting then it calls ScreenSaverForm.Refresh()
to evoke a paint
event. Only refresh works on all screens on a system.
When the user moved, clicks a mouse or presses a button, the corresponding event calls.
ShuttingDownDel()
is the delegate that sets ScreenSaver.ShuttingDown = true
. Then the ScreenSaver.RunMeTillShutDown
calls the method below to close all the forms.
Here is the CloseMe
method of the ScreenSaverForm
...
public void CloseMe()
{
stopNow = true;
for (int i = 0; i < SwarmCount; i++)
insects[i].stopNow = true;
Close();
}
There are standard mouse and keyboard routines that set calls CloseMe()
if the user moves or clicks the mouse or presses a key.
Finally, we have the paint
event itself ....
protected override void ScreenSaverForm_OnPaint(PaintEventArgs e)
{
if (stopNow == false)
{
try
{
for (int i = 0;( i < SwarmCount) && (stopNow == false); i++)
{
insects[i].DrawWaspThenSwarm(e.Graphics);
SuspendLayout();
e.Graphics.Flush(
System.Drawing.Drawing2D.FlushIntention.Flush);
ResumeLayout();
}
}
catch (InvalidOperationException ev)
{
EventLog.WriteEntry("SwarmScreenSaver" +
ev.Source,ev.Message,
EventLogEntryType.Error);
}
}
DonePaintingDel(ScreenNumber);
}
Well, that's about it. There are some try
catch
stuff here that I did not put in the snippets.
There are some Lock statements in the Insects
classes. One locks the e.Graphics
object passed in as well as drawing functions from being accessed by other objects until the instance is finished with drawing.
There is a lot more code in one more module, that would be clsInsects.cs but if I were to comment on that, this article would be twice the size!
Points of interest
Don't forget to look at the HTML with your browser. This is found in CodeCommentReport\Solution_SwarmScreenSaver.HTM
History
Discovered that running instances of ScreenSaverForm
on the main thread via the invalidate
and having the drawing code in the paint
event which in turn used to call Invalidate()
to draw the next frame was the best way to go than on worker threads, or timer events. Even better yet is the messaging and the WaitHandle.WaitForAny()
method spoken about below.
- I found that I did not have to clear the source bitmap every time I finished drawing if I did not use anti-aliasing and this was before I did double buffering! In order for my code to erase a line, I would actually draw a black line over the old line. Well, it seems that the GDI+
DrawImage
is smart enough not to count a line that is of the same color as the background as something to copy, but only draws what is visible and skips all else. This becomes evident if you choose a different color for the erasing, the drawing slows down.
- I found that the form's double-buffering stuff absolutely screams!! The program now draws faster than ever! This auto-double-buffering does not carry the contents of the screen across calls to
paint
events so you do not have to draw over old stuff to simulate erasing if you are using this type of double-buffering.
- I could wrap imports from DLL's into wrappers! This sure makes things easy, to have all your special code in one place. By special is the code you find yourself needing to execute a lot but isn't part of a mission critical section of code.
I moved the API calls into the CommonFunctions
class and wrote wrappers for these functions because I could not call these directly via an instance of the class. The wrapper is the same name as the API call but with an API at the end so ...
Code like ...
if (IsWindowVisible(ParentWindow) == true) becomes ....
if (cf.IsWindowVisibleApi(ParentWindow) == true ...
Got this reply quoted in italics below from rgesswien on an exception in checking processes. Use at your own risk, however, I'm going to remove the check for multiple instances because as far as I can tell, I don't even need it. I've seen a few screensavers with it and a few without it, but in my testing I found that my program was terminated by Windows anyway without me needing that code.
FWIW: If you get an InvalidOperationException
on GetCurrentProcess
in the function IsInstanceAlreadyRunning()
here is the fix. (At least for me.) Regedit
to HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\PerfProc\Performance Change the value of "Disable Performance Counters" from 1 to 0. (This error is all over the web but I could only find one thread in a newsgroup that explained how to fix the problem.)
I no longer use IsInstanceAlreadyRunning()
because my OS terminates the program when going between CONFIG, mini-preview and full-preview. I also got a note about the CONFIG screen not being usable by the user entering /c, but assuming it was brought up by the Display Properties window. I will fix this.
Major changes! I've at the advice of an authority on GDI, I have moved my paint routines except the one for the preview, into a paint
event in ScreensaverForm
.
I read that it was a very bad idea to do drawing to the screen outside of an event, so there is no more SaverLoop
. The drawing stuff is initialized in the load
event, but all drawing occurs within the paint cycle.
6/2/2005 Issues ...
I have now included the saving of CONFIG info to an XML file.
- I finally was able to get my system's two monitors working in duel view mode. I have found that I used to get the same issues as everyone else who has tried the multiple monitor approach in that one monitor gets the screensaving but the second monitor, that I started the saver on gets no processing for the screensaver. Further comments below regarding multiple screen screensavers.
- I looked at all the screensavers on CodeProject, and according to the replies, every one of them has the same issue. Given that cloning and spanning seem to work, but duel view has the issue, and only one monitor is useful in this mode or assign apps to run on a particular monitor. I have solved this issue as mentioned above.
Latest change as of 06/04/2005
Configuration is now saved as an XML file and it's a lot simpler. Your CONFIG file will be created if it doesn't' exist, and will be the name of the executing file + .config.xml. Example. Swarm.scr creates Swarm.scr.config.xml. The changes to the code were minor, so I won't change the snippets above, because the concept is the same except minor changes and re-doing the snippets is a nightmare so I've just uploaded a new source file zip. If you wish, just look at the code and code comments web pages included to see the changes. Notice the extra file XmlSwarmScreenSaverConfig.cs.
I have also added a call to a parsing routine in the entrypoint.cs' case
statement to more clearly parse the command line, so you can better see what is happening.
06/07/2005
Just cleaned up some horrible grammar in the article and references to SaverLoop that no longer exists. Isn't it funny how trying to get rid of spelling and grammar errors is like trying to vacuum up all the sand out of a shag carpet that has been washed up on a beach?
06/10/2005 - New things added ...
- Added some code to the
insects
class which does my drawing, so that it will stop immediately if the form it is drawing on is in the process of closing. I hope this might solve the un-catchable InvalidOperationException
I was getting.
- Added the ability of the saver to handle multiple swarms and wasp groups on the same screen!! It really looks neat, having several swarms dancing about. I found that the swarms would start out in about the same area and then diverge, with some swarms going off on their own, and then strangely, all the swarms moving in sync as in a dance! It was very nice to watch! I accomplished this by changing the single instance of the
insects
class to an array. I added a configuration option where you can specify between 1 and 100 swarms, so if you feel like going nuts, go for it, give that computer a REAL stress test!
- Added code to the XML CONFIG saver reading method, so that if it discovers that a field is missing, in this case, the new
SwarmsPerScreen
field or is corrupted in some other way, it self recovers by recreating a new configuration file with default values and so no worries about forgetting to remove the old CONFIG file.
- Discovered that the
insects
class was using a STATIC instance of the CommonFunctions
class which is used to derive random numbers, color adjustment and other functions, and this could very well explain why it failed to run on multiple monitors. Later, I may uncomment out the multiple monitor code and give it a try, but not just yet. Obviously, if one is having multiple objects addressing a single instance, this can be a bit of a problem.
Enjoy!, I really like these dancing swarms!!
6/11/2005
Hopefully fixed issue with object in use exception. It seems the issue was caused by my process trying to access the Graphics
object in the paint
event before the screen was done painting. I could have put in some sort of waiting between calls, but I wanted to have the thing run as fast as possible, so I simply put a lock(e.Graphics)
statement on the Graphics
object passed in the paint
event argument e
. This seems to have cleared it up, but as with all intermittent bugs it has a life of its own, thus proving the truth of my suspicion that C# is an upgrade of Murphy's C++!
I have just succeeded in getting this screensaver to work on multiple monitors in duel mode on a single system at the same time!!
6/15/2005
In an ongoing effort to solve the InvalidOperationException
event, "Object in use elsewhere" issue, I have utilized graphics paths instead of drawing one line at a time, I build the graphics and then draw the graphics path. I use the Graphics
object's CloseFigure()
method which starts a new subfigure which causes each bee to be considered as a separate subfigure otherwise there would be a line connecting the bees together. The effect was like a wad of paper in model form. It was interesting to look at.
I use delegates to inform the ScreenSaver
class when a form is shutting down and another to inform when it's done painting, and set a boolean array so it knows which forms are ready to draw. The boolean array isn't necessary because I discovered that if one form needs a refresh, the other did too by the time it got to the refresh.
I find it interesting that the Application.DoEvents()
method and program events are tied together, in other words, there is no way to control what gets stopped. If I don't call DoEvents
regularly, the form events stop. I can't just arbitrarily call Application.Events()
without the forms getting a repaint before it's ready. The paint
event draws the graphics, but it will never appear on the monitor until Application.DoEvents()
is called which of course effect all screens!
I have separated the mini-preview and FullScreen Preview/Screensaver code into MiniPreview.cs and ScreenSaver.cs.
Update late 06/18/2005
Found a method of eliminating wasting CPU cycles just running a loop waiting for an event instead of waiting for the event gracefully. One thing I know about Windows, is that it is message driven, and when I started implementing the event driven ScreenSaver
class, I looked for a way to wait for an event instead of wasting CPU cycles.
I found a method called WaitHandle.WaitForAny(Array[])
. This method takes an array of ManualResetEvents
or AutoResetEvents
and can be used to stop the current thread wait till any of the event instances in the array is set via manualEvent.Set();
. By using this one statement and then using a manualEvent.Reset()
when a ScreenSaverForm.Refresh()
was about to be called, I was able to cause the thread to sleep and wait for the events instead of gobbling CPU cycles like a school of hungry piranha and overburdening the computer.
By using the WaitForAny
method as mentioned above, I was able to eliminate a source of much un-necessary burdening on the system. The code changes I did are in bold in the snippets above.
Update for 8:PM PST 06/19/2005
Due to the tenacity of the InvalidOperationEvent
issue, I have added a CONFIG option to toggle the style based double buffering in favor of my manual double-buffering code, which has never generated this bug, but is much slower. Caveat: For some reason, when I execute the program within the development environment and a new option is found that is not in the CONFIG file the IDE hangs. If I run the app outside the development environment or delete the old CONFIG file, it does not hang.
I've uploaded the new source code. I have left the snippets the same because there were very few changes that really mattered.
I think I have found what is the cause of the InvalidOperationException event...
The manual double buffering never crashes. In this kind of buffering there is no double buffer between the Graphics
object, and the display, and so there is no guess work about when this buffer is written to the screen because I get complete control over this via the code.
The control's built in double buffering is automatic in that the control has a back buffer that is written at a time decided by the runtime unless you call the flush
method mentioned earlier. In this scenario the control has to guess when it's time to copy the back buffer to the display and also guess when it's time to write the buffer, and it tends to choose poorly. The control under double-buffering via the setstyle was deciding to copy the buffer to the screen and dispose it just as my program tries to write to this buffer!
This would explain the delay time where I have seven swarms on the screen, but one only appears every 4 seconds, till they are all on-screen.
Updates 06/20/2005
Looks like, the above theory is possibly true. I got a reply from Bob Powell stating that he suspected there was some sort of bug with auto-double buffering, but it's not confirmed, except that the horror bug has been solved. It's been running for about 15 hours without crashing so the issue is solved after I did the change below ...
Changes ...
I removed a lot of whining and commentary about the error I spoke of. I allowed my judgment to be affected by my frustration and ranted about it. While I'm sure, it was interesting to me, it was probably boring to you. I apologize.
Important change 06/24/2005
I have found the cause of the InvalidOperationException
error. I was drawing outside of the buffer, forcing it to dispose and resize! Yep, it was my fault! I had assumed that the Graphics
object I get in paint
event arguments would have its clipping region set to the size of the form. I was wrong. By default it appears to be infinite. When drawing is done with double-buffering and this drawing happens out of bounds, the backbuffer in auto-double buffering will destroy itself and resize. This was causing the exception. I tried setting it via the bounds of the form but when the computer is running in multi-screen, the bounds and the clip bounds in the Paint
event args for the secondary monitor have an offset in the X and Y values. Here is what I have done about this ..
- Although using
Graphics.SetClip(e.ClipRectangle)
seems to work, I do not trust it. Instead I put the code that was originally in the swarm
class that will skip drawing of any graphic unless all of it is completely in the screen dimensions as specified in width and height and that no part of the graphic is in a negative pixel location. I might have to do some adjustments aside from this due to pixel offset, but at least I'm on the right track.
- I removed the flush statement as I don't need it now.
- Important - I found out that when a form is locked and I try to resize it in code, it causes an endless stream of paint commands trying and failing to resize the form. My code would ignore these but they would be sent again and again until I unlocked the form.
- Added several tests and a
paintStatus
variable to track the status of all paint
events done. My code now resides in the paint
event instead of OnPaint
.
- Setting paintEventArgs's
pev.Graphics.SetClip(Bounds)
was preventing the secondary monitor from drawing. This does work here but I don't trust it ... pev.Graphics.SetClip(pev.ClipRectangle);
I may get to changing the snippets later, but the source code is something you will want to re-download if you are using this for your own savers.
Well, that's it ...
Enjoy!