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

Holistic Screensavers: Beginning to End

0.00/5 (No votes)
14 Jun 2003 1  
Best practices for screensaver coding/distribution.

Contents

(c) 2003 Lucian Wischik. Anyone can use this code anyhow they want, except they can't sell it or claim ownership.

Introduction

By now everyone knows the technical details of how to write a working screen saver. This article introduces the next level of screen saver development: best practice for the entire lifecycle of the saver, from the start of writing it, right through to deploying it to end-users. And it explains how these considerations affect the saver's code itself. I call this "holistic" screen saver development.

Here you will find my distilled experience from seven years of writing 32bit screen savers: what is the best structure to the program, how best to manage installation/uninstallation, how to include basic features like JPEGs, sprites, audio and 3D, and how to save yourself from having to answer thousands of tech-support emails from users!

An important thread throughout this article is: keep things simple for the user and the developer. For the user, that means keeping the saver in just a single self-contained .scr file. For the developer, it means incorporating install/uninstall support directly into the .scr file so it's easier to deploy. Also, to help developers, this article shows how to achieve standard effects (sprites, JPEGs, audio) and how to store zipped media as resources within the .scr file, and extract them without needing temporary files on disk. The code is written in plain Win32/C++, using the Standard Template Library. I have found this to be the simplest to write and maintain.

Background

When Microsoft released Win95, it came with a new way of doing screen savers: with the "Display Properties" dialog, with a preview window and password management. Except they didn't document how to program these things, and no one knew how to program them. Then I reverse-engineered the Microsoft savers and wrote the comprehensive technical article How To Write a 32bit Screen Saver. If you have read any screen saver tutorials or downloaded any example code, chances are that they refer back to this original article. Now that everyone knows the technical nuts and bolts, and has had some experience in writing and deploying savers, it's time for a new technical article that covers a new area.

Structuring the code

Question: How to design the best screensaver framework?

Answer: Don't. After designing (and selling) lots of savers and frameworks, my final conclusion is that frameworks don't help. Instead, make a complete, self-contained, minimal saver that doesn't impose any structure on the programmer and doesn't require to be "learnt". Keep it simple. Accordingly, I here present five very basic example screen savers. Each one has the same layout: its source code is contained in a single .cpp file with the following structure.

  // Comments at the top about how it works, and how
  // to achieve the same in your own savers.

  A bunch of standard header declarations

  // -----------------------------------------------

  Then comes the code that's unique to each saver.
  This is the important thing to read in the examples.

  // -----------------------------------------------

  Then comes the generic saver code that's common
  to all of them.

The end of this article gives the top-comments for each of the five savers. These comments explain how to incorporate their functionality into your own code: MinScr (very basic); Images (transparent sprites, JPEGs, stored in a .zip in a resource); PlayOgg (playing an OGG file direct from memory - OGG is like MP3 but better); AudioIn (sampling); ThreeDee (OpenGL). But first, here are some basic code lessons.

  • Use global variables for the saver's settings. I know this goes against one's normal instinct, but in this case a saver's settings are inherently global: they're loaded from the registry upon startup, modified by the configuration dialog, and saved to the registry when you click the "OK" button. When you have a preview in your configuration dialog, then they're shared between preview and dialog. And when the saver runs on multiple monitors, the settings are shared between each monitor. If you try to pass a 'settings object' around all the time, it's messy. Just make your settings global.
  • Plan the purpose of your variables. Some things (e.g. configuration properties loaded from registry) apply globally to all saver windows and to the configuration dialog. Other things (e.g. backbuffers) are needed once for each saver window. Other things (e.g. audio input) should happen once for the entire system.
  • Allocate resources lazily (i.e. upon first use) so far as possible. That means: initialize a global handle or pointer to 0, and in your OnPaint handler or wherever where you first use it, then check whether it's still 0 and if so create it. The check gets performed every single cycle, but it's not costly.

    This code snippet, from the MinScr example, shows how you might use your settings from registry. The configuration dialog includes a preview of the saver. When a change is made in the configuration dialog, it should be reflected immediately in the preview. This is done by storing the setting in a global variable, and updating it in response to WM_COMMAND, and saving it in response to PSN_APPLY.

    // These are the saver's global settings. We use a global
    
    // refcount so that we don't load the settings more than
    
    // once. This isn't strictly necessary in such a small
    
    // example, but it's elegant.
    
    int refcount=0;      
    bool BigBlobs=false; // our only configuration setting
    
    
    void CommonInit()
    { refcount++; if (refcount!=1) return;
      //
    
      BigBlobs=RegLoad(_T("bigblobs"),true);
    }
    
    void CommonExit()
    { refcount--; if (refcount!=0) return;
      //
    
      // ... There's nothing to do here. But if CommonInit had
    
      // allocated some big storage (eg. reading a configuration
    
      // file, or precomputing an array whose size you don't
    
      // know at compiletime) then here's where to deallocate it
    
    }

    The TSaverWindow class is one that I use in all my savers. The generic saver code at the bottom of the file creates a window and an instance of this class: one instance for the preview window (be it in Display Properties or in the saver's own configuration dialog;) and one instance per monitor when it runs full-screen in a multi-monitor system. In the constructor, id is -1 for a preview, or 0 for the primary monitor, or other index for secondary monitors.

    struct TSaverWindow
    { HWND hwnd; int id;  
    
      TSaverWindow(HWND _hwnd, int _id) : hwnd(_hwnd),id(_id)
      { CommonInit(); SetTimer(hwnd,1,100,NULL);
        ...
      }
    
      ~TSaverWindow()
      { CommonExit();
      }
    }

    The configuration dialog in all good savers is a property sheet. The first page is a generic one that handles hot corners, mouse sensitivity and password delay. It's part of the generic code common to all these example savers. The second page has the options specific to this saver. It also has a monitor with a preview on it. Place the monitor in your dialog-editor with class ScrMonitor. It will automatically create an instance of TSaverWindow to go inside it.

    DLG_OPTIONS DIALOG  0, 0, 237, 220
    STYLE DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_VISIBLE
          | WS_CAPTION | WS_SYSMENU
    CAPTION "Options"
    FONT 8, "MS Sans Serif"
    BEGIN
      CONTROL  "",101,"ScrMonitor",0,52,12,123,105
      CONTROL  "Big Blobs",102,"Button",
               BS_AUTOCHECKBOX | WS_TABSTOP, 13,120,65,15
    END
    BOOL CALLBACK OptionsDlgProc(HWND hwnd,UINT msg,
                                 WPARAM wParam,LPARAM lParam)
    { switch (msg)
      { case WM_INITDIALOG:
        { CommonInit();
          CheckDlgButton(hwnd,102,BigBlobs?BST_CHECKED
                                          :BST_UNCHECKED);
        } return TRUE;
    
        case WM_COMMAND:
        { int id=LOWORD(wParam);
          bool x = (IsDlgButtonChecked(hwnd,102)==BST_CHECKED);
          if (id==102 ) BigBlobs=x;
        } return TRUE;
    
        case WM_NOTIFY:
        { LPNMHDR nmh=(LPNMHDR)lParam; UINT code=nmh->code;
          switch (code)
          { case (PSN_APPLY):
            { RegSave(_T("bigblobs"),BigBlobs);
              SetWindowLong(hwnd,DWL_MSGRESULT,PSNRET_NOERROR);
            } return TRUE;
          }
        } return FALSE;
    
        case WM_DESTROY:
        { CommonExit();
        } return TRUE;
      }
      return FALSE;
    }

    Note: I provided RegSave(..) and RegLoad(..) functions for integers, bools and strings. That's because they're so common, and needed by every saver ever. They store their stuff in the key HKEY_CURRENT_USER\ Software\ Scrplus\ <savername>. The _T("text") macro is defined in the header <tchar.h>, and provides interchangeability with Unicode and ASCII: if you compile with Unicode, then it's treated as a Unicode string; compile with ASCII and the compiler sees it as ASCII. All of the examples can be compiled under either setting.

    The following code comes from the Images example. It uses double-buffering to reduce flicker. Each saver needs its own back buffer. Therefore, the buffer is part of TSaverWindow. Observe also in the following code how we allocate it lazily. This is a nicer coding paradigm, since the code is more resilient. (i.e. every time you use an allocate, you are also explicitly verifying its validity).

    struct TSaverWindow
    { HWND hwnd; int id;  
      HBITMAP hbmr;  // the back buffer
    
      //
    
      TSaverWindow(HWND h,int i) : hwnd(h), id(i), hbm(0)
      { 
      }
    
      ~TSaverWindow()
      { if (hbm!=0) DeleteObject(hbm); hbm=0;
      }
    
      void OnPaint(HDC hdc,const RECT &rc)
      { int w=rc.right, h=rc.bottom;
        if (hbm==0) hbm=CreateCompatibleDC(hdc,w,h);
         
        HDC bufdc=CreateCompatibleDC(hdc);
        SelectObject(bufdc,hbm);
        // ... compose our image into bufdc
    
        // ... once it's done, blt it to the screeen:
    
        BitBlt(hdc,0,0,w,h,bufdc,0,0,SRCCOPY);
        DeleteDC(bufdc);
      }
    };
  • Multiple monitors - their time has come! Program from the ground up with multi-monitors in mind. It's usually too hard to retrofit support at a later stage. Plan in advance: which variables are global (shared between all screens and maybe also the configuration dialog), which are specific to a particular screen. Music, for instance, shouldn't be played by every monitor's saver window.

    The following code comes from the AudioIn example. In it, a single line builds a "voiceprint" across every monitor, from left to right. This drawing is shared to every instance of TSaverWindow, so we do it globally. (The code here focuses just on how to structure your code for multi-monitors; to find out how to use the example code for audio-sample and Fourier-transforms, follow the link to the example).

    void DrawInit();  
    void Draw();      
    // These are global functions, since they're shared by all
    
    // TSaverWindow instances
    
    
    
    struct TSaverWindow
    { HWND hwnd; int id; 
    
      TSaverWindow(HWND _hwnd,int _id) : hwnd(_hwnd),id(_id)
      { CommonInit();
        // id==-1 for a preview window,
    
        // or id==0 for the primary monitor
    
        if (id<=0) {DrawInit(); SetTimer(hwnd,1,0);}
      }
    
      ~TSaverWindow()
      { if (id<=0) KillTimer(hwnd,1);
      }
    
      void OnPaint(HDC hdc,const RECT &rc)
      { FillRect(hdc,&rc,(HBRUSH)GetStockObject(BLACK_BRUSH));
      }
    
      void OnTimer()
      { Draw();
        // note: only id<=0 allocated a timer
    
      }
    };
    int linex; RECT rcl;
    // We'll draw a single vertical line, xcoord 'linex',
    
    // stepping over each monitor. Rcl is its enclosing box,
    
    // probably larger than just the primary monitor.
    
    
    void DrawInit()
    { rcl.top = (monitors[0].top+monitors[0].bottom-300)/2;
      
      // Which will be the leftmost monitor? The rightmost?
    
      // Start from the primary monitor and trace left and
    
      // right to find them.
    
      rcl.bottom = rcl.top+300;
      rcl.left=monitors[0].left; rcl.right=monitors[0].right;
      for (vector<RECT>::const_iterator i=monitors.begin()+1;
           i!=monitors.end(); i++)
      { if (i->bottom>rcl.top && i->top<rcl.bottom)
        { rcl.left = min(i->left, rcl.left);
          rcl.right = max(i->right, rcl.right);
        }
      }
      linex=rcl.left;
    }
    
    void Draw()
    { linex++; if (linex>=rcl.right) linex=rcl.left;
    
      unsigned int m=0;    // Find out which monitor we're on
    
      for (vector<RECT>::const_iterator i=monitors.begin();
           i!=monitors.end(); i++, m++)
      { if (linex>=i->left && linex<i->right &&
            i->bottom>rcl.top && i-*>top<rcl.bottom) break;
      }
    
      HWND hwnd=SaverWindow[m]->hwnd;
      HDC hdc=GetDC(hwnd); 
      int relx=linex-monitors[m].left;
      int rely=rcl.bottom-monitors[m].top;
      MoveToEx(hdc,relx,rely,NULL); LineTo(hdc,relx,rely-300);
      ReleaseDC(hwnd,hdc);
    }

    Note: if you've only got a single monitor, you can still test multi-monitor support with "fake" monitors by running your saver with arguments.

    mysaver.scr /sm         // test multi-monitor
    
    mysaver.scr /p scrprev  // test preview

Deploying the saver

Question: How to deploy savers?

Answer:

  1. Embed all the artwork and extra files within the saver itself, as resources.
  2. Build installer/uninstaller support directly into the saver.

By embedding the artwork, it makes it easier for users to stay in control of their hard disks. By in-building installer/uninstaller support, it becomes easier to develop the saver: there is just a single project to build in a single development environment, entirely self-contained, and the installer here is much slicker and faster than what you get with InstallShield or MSI. That's important, because users typically download lots of savers to try out, and so need to uninstall them equally quickly.

  • Single self-contained file for the saver. Don't use any additional files: instead, embed all your artwork and music as resources within the saver. This means you have to compress them. I have included example savers to show how to uncompress and use resources that are OGG music files, ZIP files and JPEG pictures. I rewrote the standard public OGG and ZIP source code to make them easier to use, and (in the latter case) to avoid the need for temporary files: instead, zipping happens directly from resource to memory buffer.

    The following code comes from the Images example saver. This saver has a RT_RCDATA resource which is a zip file, and in the zip file is a bitmap and a JPEG. Zip support comes from a module called unzip.cpp/unzip.h. This is basically the Info-zip source code, but I tidied it up into a single .cpp file and gave it a more Windows-ish API. (My unzip.cpp has also been used in another CodeProject article).

    // Lock the ZIP resource in memory.
    
    // (error-checks have been omitted here for clarity)
    
    HRSRC hrsrc=FindResource(hInstance,_T("ZIPFILE"),RT_RCDATA);
    DWORD size=SizeofResource(hInstance,hrsrc);
    HGLOBAL hglob=LoadResource(hInstance,hrsrc);
    void *buf=LockResource(hglob); 
    
    HZIP hzip=OpenZip(buf,size,ZIP_MEMORY); 
    
    ZIPENTRY ze;
    ZRESULT zr=FindZipItem(hzip,"background.jpg",true,NULL,&ze);
    if (zr==ZR_OK)
    { HGLOBAL hglob = GlobalAlloc(GMEM_MOVEABLE,ze.unc_size);
      void *buf=GlobalLock(hglob);
      UnzipItem(hzip,index,buf,ze.unc_size,ZIP_MEMORY);
      GlobalUnlock(hglob);
      hbmBackground = LoadJpeg(hglob);
      GlobalFree(hglob);
    }

    The focus here is on how to uncompress media from a zip resource. The functions for LoadJpeg() and for making a transparency mask are discussed elsewhere, in the Images example.

    zr=FindZipItem(hzip,"sprite.bmp",true,NULL,&ze);
    if (zr==ZR_OK)
    { vector<byte> vbuf(ze.unc_size); byte *buf = &vbuf[0];
      // the magic of STL will deallocate vbuf automatically
    
      // at end of scope
    
      UnzipItem(hzip,index,buf,ze.unc_size,ZIP_MEMORY);
      BITMAPFILEHEADER *bfh=(BITMAPFILEHEADER*)buf;
      BITMAPINFOHEADER *bih=(BITMAPINFOHEADER*)
                            (buf+sizeof(BITMAPFILEHEADER));
      int ncols=bih->biClrUsed;
      if (ncols==0) ncols = 1<<bih->biBitCount;
      char *srcbits=(char*)(buf+bfh->bfOffBits), *dstbits;
      hbmSprite=CreateDIBSection(NULL,(BITMAPINFO*)bih,
                      DIB_RGB_COLORS,(void**)&dstbits,NULL,0);
      unsigned int numbytes = bih->biSizeImage;
      if (numbytes==0) numbytes=((bih->biWidth*
               bih->biBitCount/8+3)&0xFFFFFFFC)*bih->biHeight;
      CopyMemory(dstbits,srcbits,numbytes);
      //
    
      BITMAP bmp; GetObject(hbmSprite,sizeof(BITMAP),&bmp);
      int w=bmp.bmWidth, h=bmp.bmHeight; 
    }
    
    CloseZip(hzip);
  • Combine installer/uninstaller/saver in a single executable. That is, create a single executable called saver_setup.EXE. When the user runs it, it installs itself (including uninstall support) by copying itself into the Windows directory but renamed as saver.SCR. There it behaves like a normal saver. When it's run with the argument /u, it behaves like an uninstaller.

    This simple scheme is magical. It simplifies your build processes, since you need only build a single file (rather than building in one development environment and then making an installer in another). It also benefits from my long experience in installer-related problems that users have had! And best of all, you can offer two downloads: one to a self-installing executable which the users just double-click to install, and the other to a .scr file which advanced users can download and test. And in reality, they're the same file -- on a web server you could even store them as hard links to the same binary. The cost is that you have to compress all your media internally as resources, rather than relying upon some external compressing installer. But we've already seen how to use compressed resources, so this isn't a problem.

    The installer is implemented in the generic code that's common to all the example savers. It is launched whenever the file has the .EXE suffix and is not given any arguments. It looks inside its resources for STRINGTABLE resource #1, and uses this as the filename. (Note: it's a technical requirement of all screensavers that they store their name as string#1, and also that they use the same filename, and that they are placed in the Windows directory). String#2 is used by the installer as the help-link in Add/Remove Programs. And remember to change the VERSIONINFO, and also the manifest!

    // This is the saver's resource (.rc) file. I personally
    
    // like to edit .rc files with notepad rather than a visual
    
    // resource editor. But it's up to you how you edit it.
    
    
    1        ICON            "playogg.ico"
    1        RT_MANIFEST     "manifest.txt"
    
    ZIPFILE  RCDATA          "test.zip"
    
    STRINGTABLE 
    BEGIN
        1                    "Play Ogg"
        2                    "http://www.wischik.com/lu/scr"
    END

Beware bandwidth. In August 2002 I put a saver on my website, then went away on a holiday. When I came back in September, it had been downloaded hundreds of thousands of times, and I was lumped with a US$400 bill for excess bandwidth for that month alone. Be careful!

Debugging and Deployment

Included in the download is my scrprev utility. This is to help with debugging the Preview feature. First thing you do should be to copy this into your Windows directory.

Build your saver with the extension .SCR, and test it with these arguments:

     <no arguments>  to test the configuration dialog
     /p scrprev      to test the preview mode
     /s              to run full-screen
     /sm             to test multi-monitor support

Normally, when savers run, they run topmost and they quit themselves when there is mouse movement. This is a bit obnoxious when it comes to debugging their full-screen functionality, since you can't single-step through with a debugger! To help with debugging, look for two lines near the start of the file:

const tstring DebugFile = _T("OutputDebugString");
const bool    SCRDEBUG  = (DebugFile!=_T(""));

The first says that interesting events will be logged. You can leave it as OutputDebugString, or give it a filename, or make it empty to turn it off. Use Debug(_T("a message") to log your own messages. The flag SCRDEBUG suppresses the obnoxious behavior mentioned above, so as to help you with debugging. Before deployment, remember to turn these two features off.

  • Watch for gradual leaks. A screen saver might run for hours or days on end. If there is any memory or resource leak, it will eventually cause problems. Be very careful, and use the Task Manager to monitor program size and GDI heap. You need to be explicitly aware of every time you allocate and deallocate large blocks of memory. In particular, DON'T write "generic cleanup" destructors that clean up if necessary upon termination. Because without them, any failure to deallocate on time will be flagged by your memory-leak-detector (assuming you have one) and so the leaks will show up much earlier in your debugging phase.

Once you've finished debugging, rename your saver with a name like MySaver-setup.exe. Then, when the user double-clicks it, it will run its built-in installer.

Using the examples

To create a new saver, I copy the directory from an example, rename all the files, open them all up in Notepad and alter the names using search/replace. Don't forget the STRINGTABLE resources in the .rc file, or the VERSIONINFO, or the manifest.

The example savers are described below. In particular, for each saver, I explain how it achieves its particular effect, and how to add the same functionality to your own code.

  • MinScr - doesn't do anything special, just blobs on the screen using double buffering.
  • Images - JPEGs, bitmaps, transparency for sprites, using a zipped data file embedded as a resource.
  • PlayOgg - plays audio in the background, from an OGG file embedded as a resource. (OGG is like MP3 but better).
  • AudioIn - listens to the currently-playing audio and responds to it, with fast Fourier transforms.
  • ThreeDee - 3D graphics with OpenGL, changing screen resolution.

Each example comes with project files for Visual C++6, Visual Studio .NET and Borland C++ Builder 5. I'm sure the code will also work fine under other compilers. If you want, you can delete the project files for the compilers you're not using. The extensions are:

   .sln .vcproj    -  Visual Studio .NET workspace/project
   .dsw .dsp       -  Visual C++6 workspace/project
   .bpg .bpf .bpr  -  Borland C++Builder5 group/project

If you want to create a new project entirely from scratch, here are some tips. All of these are basic Visual C++ knowledge, but many beginner programmers don't seem to know them.

  1. Copy the relevant .cpp code from an example, and .rc;
  2. Under Project > Settings > Linker, add comctl32.lib, and also winmm.lib if you're using audio;
  3. None of my examples use precompiled headers, so turn them off under Compiler > PrecompiledHeaders;
  4. You should always go to Compiler > General and set Warning Level to the highest - except with VC++ 6 which can't cope properly with the STL;
  5. Similarly, go to Compiler > Preprocessor and add the STRICT definition;
  6. For your Release build, under the general Project > Settings, turn on Global Optimizations. Also turn them on under Compiler > Code Generation.

And if you didn't understand the purpose of any of these steps, then read about them in the online help!

MinScr example

This saver takes a snapshot of the desktop upon startup, keeps the snapshot as a back-buffer, and draws blobs on it. There's a configuration option to change the size of the blobs.

The basic things to change are marked TODO. They are: load/save the global settings, create an animation for the saver, make its Configuration dialog, and set its name in the Stringtable resource. But the source code is very short and clear, so the programmer should feel confident to understand and modify all of it.

The following code shows how this saver gets a snapshot of the desktop upon startup. It's a nice idea, but doesn't really work under NT/Win2K/XP, since they run their savers on a separate desktop and so won't see anything. Oh well.

struct TSaverWindow
{ HWND hwnd; int id;
  HANDLE hbm;        // we store the snapshot here


  TSaverWindow(HWND _hwnd,int _id) : hwnd(_hwnd),id(_id)
  { // If we're a preview window, we snapshot the primary

    // desktop. If we're a full-screen saver, we snapshot

    // whatever monitor we were running on.

    RECT rc; GetClientRect(hwnd,&rc); 
    int w=rc.right, h=rc.bottom;
    //

    if (id>=0) GetWindowRect(hwnd,&rc);
    else
    { rc.right=GetSystemMetrics(SM_CXSCREEN);
      rc.bottom=GetSystemMetrics(SM_CYSCREEN);}
    }
    //

    HDC sdc=GetDC(0), bufdc=CreateCompatibleDC(sdc);
    hbm=CreateCompatibleBitmap(sdc,bw,bh);
    SelectObject(bufdc,hbm);
    SetStretchBltMode(bufdc,COLORONCOLOR);
    StretchBlt(bufdc,0,0,w,h, sdc,rc.left,rc.top,
               rc.right-rc.left,rc.bottom-rc.top,SRCCOPY);
    DeleteDC(bufdc);
    ReleaseDC(0,sdc);
  }

  ~TSaverWindow()
  { if (hbm!=0) DeleteObject(hbm); hbm=0;
  }
};

Images example

This saver shows a sprite (.BMP) bouncing over a background (.JPG). The two images have been compressed in a ZIP file, and this zip is stored as a RT_RCDATA resource in the .scr file. At runtime, the zip/BMP/JPG are all extracted directly from resource into memory: no temporary files are used.

It's important to keep file-size down. That's why we use zips. (Actually, only about 10k was saved by compressing the JPG into the zip... it's uncompressed things like BMPs that really need zips.)

As for the sprites, this saver demonstrates several techniques:

  • How to load JPEGs from memory. If you want to add this to your own code, you must #include <ole2.h> and <olectl.h> and copy the LoadJpeg() function.
  • How to load BMPs from memory. The code is in EnsureBitmaps().
  • How to make transparent sprites. The sprite is actually stored as two bitmaps, the AND mask hbmClip and the OR mask hbmSprite. The code for loading/generating the two is in EnsureBitmaps(). The code for drawing them is in OnPaint().
  • How to use a double-buffering to avoid flicker. The bitmap hbmBuffer stores our back-buffer. It's created in TSaverWindow() and used in OnPaint().
  • How to read zip files. The code for this is in EnsureBitmaps(). Also, it's in the separate module UNZIP.CPP/UNZIP.H, which consists largely of code from here. Thanks, info-zip!
// To load a jpeg:


HGLOBAL hglob = GlobalAlloc(GMEM_MOVEABLE,size);
void *buf=GlobalLock(hglob);
// now copy the raw jpeg data into this buffer

// eg. by reading from disk, or from a locked resource

GlobalUnlock(hglob);
HBITMAP hbm = LoadJpeg(hglob);
GlobalFree(hglob);
...
DeleteObject(hbm);


// LoadJpeg: from an HGLOBAL containing jpeg data, we

// load it. (error-checking has been omitted, for clarity)

HBITMAP LoadJpeg(HGLOBAL hglob)
{ IStream *stream=0;
  CreateStreamOnHGlobal(hglob,FALSE,&stream);
  IPicture *pic;
  OleLoadPicture(stream,0,FALSE,IID_IPicture,(void**)&pic);
  stream->Release();
  HBITMAP hbm0=0; pic->get_Handle((OLE_HANDLE*)&hbm0);
  //

  // Now we make a copy of it into our own hbm

  DIBSECTION dibs; GetObject(hbm0,sizeof(dibs),&dibs);
  if (dibs.dsBm.bmBitsPixel!=24) {pic->Release(); return 0;}
  int w=dibs.dsBm.bmWidth, h=dibs.dsBm.bmHeight;
  dibs.dsBmih.biClrUsed=0;
  dibs.dsBmih.biClrImportant=0;
  void *bits;
  HDC sdc=GetDC(0);
  HBITMAP hbm1=CreateDIBSection(sdc,
        (BITMAPINFO*)&dibs.dsBmih,DIB_RGB_COLORS,&bits,0,0);
  //

  HDC hdc0=CreateCompatibleDC(sdc);
  HDC hdc1=CreateCompatibleDC(sdc);
  HGDIOBJ hold0=SelectObject(hdc0,hbm0);
  HGDIOBJ hold1=SelectObject(hdc1,hbm1);
  BitBlt(hdc1,0,0,w,h,hdc0,0,0,SRCCOPY);
  SelectObject(hdc0,hold0); SelectObject(hdc1,hold1);
  DeleteDC(hdc0); DeleteDC(hdc1);
  ReleaseDC(0,sdc);
  pic->Release();
  return hbm1;
}

The following code is for doing a sprite with transparency. It'd probably be easier to use TransparentBlt, but that has problems on multiple desktops and doesn't work under Win95.

// To create a bitmap from memory (eg. from a resource)

// Let 'buf' be the start address of the raw BMP file's data


BITMAPFILEHEADER *bfh = (BITMAPFILEHEADER*)buf;
BITMAPINFOHEADER *bih = (BITMAPINFOHEADER*)(buf
                          + sizeof(BITMAPFILEHEADER));
int ncols=bih->biClrUsed;
if (ncols==0) ncols = 1<<bih->biBitCount;
char *srcbits=(char*)(buf+bfh->bfOffBits), *dstbits;
HBITMAP hbm=CreateDIBSection(NULL,(BITMAPINFO*)bih,
                  DIB_RGB_COLORS,(void**)&dstbits,NULL,0);
unsigned int numbytes = bih->biSizeImage;
if (numbytes==0) numbytes=((bih->biWidth*bih->biBitCount/8
                            +3)&0xFFFFFFFC)*bih->biHeight;
CopyMemory(dstbits,srcbits,numbytes);
BITMAP bmp; GetObject(hbmSprite,sizeof(BITMAP),&bmp);
int w=bmp.bmWidth, h=bmp.bmHeight; 
...
DeleteObject(hbm);


// To create the AND/OR masks to treat 'hbm/w/h' as a sprite

// with transparency. (We take the top-left-pixel-color as

// transparent)


vHDC screendc=GetDC(0);
HDC bitdc=CreateCompatibleDC(screendc);
HGDIOBJ holdb = SelectObject(bitdc,hbm);
SetBkColor(bitdc,RGB(0,0,0));
//

struct MONOBITMAPINFO {BITMAPINFOHEADER bmiHeader;
                       RGBQUAD bmiColors[2];};
MONOBITMAPINFO bmi; ZeroMemory(&bmi,sizeof(bmi));
bmi.bmiHeader.biSize=sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth=w;
bmi.bmiHeader.biHeight=h;
bmi.bmiHeader.biPlanes=1;
bmi.bmiHeader.biBitCount=1;
bmi.bmiHeader.biCompression=BI_RGB;
bmi.bmiHeader.biSizeImage=((w+7)&0xFFFFFFF8)*w/8;
bmi.bmiHeader.biXPelsPerMeter=1000000;
bmi.bmiHeader.biYPelsPerMeter=1000000;
bmi.bmiHeader.biClrUsed=0;
bmi.bmiHeader.biClrImportant=0;
bmi.bmiColors[0].rgbRed=0;
bmi.bmiColors[0].rgbGreen=0;
bmi.bmiColors[0].rgbBlue=0;
bmi.bmiColors[0].rgbReserved=0;
bmi.bmiColors[1].rgbRed=255;
bmi.bmiColors[1].rgbGreen=255;
bmi.bmiColors[1].rgbBlue=255;
bmi.bmiColors[1].rgbReserved=0;
hbmAnd=CreateDIBSection(screendc,(BITMAPINFO*)&bmi,
                   DIB_RGB_COLORS,(void**)&dstbits,NULL,0);
//

// Now create a mask, by blting the image onto the

// monochrome DDB, and then onto DIB. (If you blt straight

// from image to DIB, then it chooses the closest mask

// colour, not absolute mask colour).

HDC monodc=CreateCompatibleDC(screendc);
HDC maskdc=CreateCompatibleDC(screendc);
HBITMAP hmonobm = CreateBitmap(w,h,1,1,NULL);
HGDIOBJ holdm = SelectObject(monodc,hmonobm);
HGDIOBJ holdmask = SelectObject(maskdc,hbmClip);
COLORREF transp = GetPixel(bitdc,0,0);
SetBkColor(bitdc,transp);
// use top-left pixel as transparent colour

BitBlt(monodc,0,0,w,h,bitdc,0,0,SRCCOPY);
BitBlt(maskdc,0,0,w,h,monodc,0,0,SRCCOPY);
// the mask has 255 for the masked areas, and 0 for the

// real image areas.

//

// Well, that has created the AND mask. Now we have to zero-

// out the original bitmap's masked area to make it an OR.

BitBlt(bitdc,0,0,w,h,monodc,0,0,0x00220326);
// 0x00220326 is the ternary raster operation 'DSna', which

// is reverse-polish for "bitdc AND (NOT monodc)"

SelectObject(maskdc,holdmask); DeleteDC(maskdc);
SelectObject(monodc,holdm);
DeleteDC(monodc); DeleteObject(hmonobm);
// 

SelectObject(bitdc,holdb); DeleteDC(bitdc);
ReleaseDC(0,screendc);


// To paint transparently to 'hdc' using these AND/OR masks:


HDC memdc=CreateCompatibleDC(hdc);
SelectObject(memdc,hbmAnd);
BitBlt(bufdc,x,y,sw,sh,memdc,0,0,SRCAND);
SelectObject(memdc,hbm);
BitBlt(bufdc,x,y,sw,sh,memdc,0,0,SRCPAINT);
DeleteDC(memdc);

PlayOgg example

This saver plays music from an OGG file that's embedded as a resource. I used to think music in a screen saver was silly (because savers are run when you're not at the computer!) But I was wrong: for some of my musical savers, I've had lots of happy parents write to me that their young children are transfixed by the combination of music and images. OGG is a file format that's like MP3, only better.

The chief user-interface concern is that lots of users have left sound muted in their system tray (or in the saver preferences), but don't realize it, and they email me to complain that sound isn't working. My solution is:

  1. In the installer, if sound is off (either in the system tray or just of savers), we ask if we should turn it on.
  2. In the configuration dialog, if sound gets turned on in the dialog, then we turn on sound in the system tray and also for savers.

As for the technical side of playing OGG files, we use a single thread that runs in the background. The application can post "play" or "stop" or "quit" messages to it. The OGG code comes from here. Thanks!

To add this OGG-playing functionality to your own savers:

  1. Under Project Settings > Linker > Input, link against winmm.lib.
  2. Copy all the code for the OGG thread, and the AudioStart...AudioQuit functions;
  3. If you want to do the Mute stuff, this is contained in the MuteControl() function, and also in modifications to the routines DoInstall() and GeneralDlgProc().
  4. Add ogg.cpp and ogg.h to your project.

OGG playback. This is handled by a single "ogg-player-thread" which runs in the background. In this article, I describe its API; to see its implementation, look at the code in audioin.cpp.

const UINT OGGM_INIT  = WM_USER+1;
const UINT OGGM_PLAY  = WM_USER+2;   
const UINT OGGM_STOP  = WM_USER+3;
const UINT OGGM_QUIT  = WM_USER+4;
//

const UINT OGGN_DONE  = WM_APP;

Launch the ogg-player-thread as follows. It's run at a high priority, so the sound doesn't break up. The while-loop-malarky is to ensure that the thread's message-queue has been created.

hthread=CreateThread(NULL,0,OggPlayerThread,0,0,&idthread);
SetThreadPriority(hthread,THREAD_PRIORITY_ABOVE_NORMAL);
while (true)
{ if (PostThreadMessage(idthread,OGGM_INIT,0,0)) break;
  Sleep(0);
}

The following code pushes an item onto the thread's playlist. The thread will take responsibility for deallocating the buffer. To play a RT_RCDATA resource, use "res://#num" or "res://name". When the song has finished naturally, and if hwnd!=0, it will post OGGN_DONE to hwnd.

TCHAR *buf=new TCHAR[MAX_PATH]; _tcscpy(buf,_T("test1.ogg"));
PostThreadMessage(idthread,OGGM_PLAY,(WPARAM)hwnd,
                  (LPARAM)buf);

To stop the music, do this:

PostThreadMessage(idthread,OGGM_STOP,0,0);
// Use wParam=0 to stop the whole playlist,

// or wParam=1 to stop just the current item


PostThreadMessage(idthread,OGGM_QUIT,0,0);
WaitForSingleObject(hthread,INFINITE);       
CloseHandle(hthread); hthread=0; idthread=0; 
// Here we tell the player-thread to terminate, then wait

// until it's done, and then close it.

Mute Control. The function bool MuteControl(bool *get,bool *set) relates to the 'Mute' button in the system tray. It retrieves the current setting into the 'get' variable, and if 'set' is non-NULL then it sets it as well. Returns true/false for whether or not it succeeded. To see its implementation, look in audioin.cpp; this article focuses instead on how (and when) to call it.

The Configuration Dialog is best done as a property sheet, with general settings (including Mute) on the first page. We alter the the dialogproc for this first page. If the user has fiddled with the Mute button, and clicks OK with it turned off, then we ensure that all muting is turned off.

BOOL CALLBACK GeneralDlgProc(HWND hwnd,UINT msg,
                             WPARAM wParam,LPARAM lParam)
{ switch (msg)
  { case (WM_INITDIALOG):
    { // use DWL_USER to track whether the user has fiddled

      SetWindowLong(hwnd,DWL_USER,0);
      // 'MuteSound' is a global variable that's read from

      // the registry at startup. 'AudioPlay' launches the

      // ogg-player-thread, and start it playing something.

      if (MuteSound) CheckDlgButton(hwnd,113,BST_CHECKED);
      else AudioPlay(hwnd);
    } return TRUE;

    case WM_DESTROY:
    { AudioQuit();      // terminates the ogg-player-thread

    } return TRUE;

    case OGGN_DONE:     // OGGN_DONE is sent to use by the

    { AudioPlay(hwnd);  // player-thread when it's finished

    } return TRUE;      // an item. So we play it again Sam!


    case WM_COMMAND:
    { int id=LOWORD(wParam), code=HIWORD(wParam);
      if (id==113 && code==BN_CLICKED)
      { // If the user changes the button, we stop

        // or start the audio

        bool x=(IsDlgButtonChecked(hwnd,113)==BST_CHECKED);
        AudioStop(); if (!x) AudioPlay(hwnd);
        // and record the fact that the user has fiddled.

        SetWindowLong(hwnd,DWL_USER,1);
      }
    } return TRUE;

    case WM_NOTIFY:
    { LPNMHDR nmh=(LPNMHDR)lParam; UINT code=nmh->code;
      switch (code)
      { case (PSN_APPLY):
        { AudioQuit();
          bool oldMuteSound=MuteSound;
          DWORD x = IsDlgButtonChecked(hwnd,113);
          MuteSound = (x==BST_CHECKED);
          WriteGeneralRegistry();
          //

          // if the user fiddled with the sound button and

          // left sound on, we also turn off muting in the

          /// system tray

          bool fiddled = (GetWindowLong(hwnd,DWL_USER)>0);
          if (!MuteSound && (oldMuteSound || fiddled))
          { bool sysmute, ok=MuteControl(&sysmute,NULL);
            if (ok&&sysmute)
            { bool nf=false; MuteControl(NULL,&nf);
            }
          }
          SetWindowLong(hwnd,DWL_MSGRESULT,PSNRET_NOERROR);
        } return TRUE;
      }
    } return FALSE;

  }
  return FALSE;
}

The following changes have been made to the DoInstall function, to prompt the user about sound.

...
ReadGeneralRegistry(); 
// reads in the global variable 'MuteSound' from the Saver

// Preferences. This is normally done ahead of full-screen/

// preview/config activation, but here we need it also

// ahead of install.


// Mow we read in the volume control in the system tray

bool sysmute, ok=MuteControl(&sysmute,NULL); 
const TCHAR *c=0;
if (MuteSound) c = _T("Note: the sound is currently ")
   _T("turned off for screen savers.\r\n")
   _T("Do you want it turned back on?");
if (ok&&sysmute) c = _T("Note: the sound has been ")
   _T("turned off.\r\nDo you want it turned back on?");
if (c!=0)
{ int res = MessageBox(NULL,c,_T("Saver Sound"),MB_YESNO);
  if (res==IDYES)
  { LONG lres; HKEY skey; DWORD disp,val;
    lres=RegCreateKeyEx(HKEY_CURRENT_USER,
                  (REGSTR_PATH_SETUP _T("\\Screen Savers")),
                  0,NULL,REG_OPTION_NON_VOLATILE,
                  KEY_ALL_ACCESS,NULL,&skey,&disp);
    if (lres==ERROR_SUCCESS)
    { val=0;
      RegSetValueEx(skey,_T("Mute Sound"),0,REG_DWORD,
                    (CONST BYTE*)&val,sizeof(val));
      RegCloseKey(skey);
    }
    if (ok&&sysmute)
    { bool n=false; MuteControl(NULL,&n);
    }
  }
}

AudioIn example

This saver samples the currently-playing audio, does an FFT, and displays it onscreen. The important end-user consideration is that the user will probably have to enable audio-input from the Sound Properties. Most users won't know how to do this. Therefore, the configuration dialog has to tell them how, and show them the results. This configuration dialog shows a waveform of the audio, has a button labeled "Try To Listen", and has detailed instructions. Here is the configuration dialog from the .rc file:

// Note: .rc files don't allow strings to be split over

// more than one line. But I've split them here to fit

// on the page. If you copy this out, you'll have to

// unsplit them. Easier instead to copy it from audioin.cpp

DLG_OPTIONS DIALOG 0, 0, 237, 220
STYLE DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_VISIBLE
      | WS_CAPTION | WS_SYSMENU
CAPTION "Options"
FONT 8, "MS Sans Serif"
BEGIN
  CONTROL "(waveform)",101,"Waveform",0x0,11,10,214,62
  CTEXT   "The saver will dance to the music, if it can "
          "hear any playing.\r\nCheck above that a signal "
          "is present.", -1,11,74,214,20
  LTEXT   "Audio troubleshooting:",-1,11,100,106,11
  LTEXT   "1.",-1,11,113,8,9
  LTEXT   "Right-click on the volume control on the taskbar"
          ", Open Volume Control > Options > Properties > "
          "Recording, OK. Then select whichever sound "
          "source you want to use. ""Mixer"" is a "
          "good bet.", -1,19,113,206,26
  LTEXT   "2.",-1,11,141,8,9
  LTEXT   "Some older soud cards don't support listening at"
          " the same time as playing ('full duplex'). In "
          "this case, audio will only work from microphone "
          "or CD, not from MP3s.", -1,19,141,206,26
  LTEXT   "3.",-1,11,168,8,9
  LTEXT   "Only one program at a time is allow to listen to"
          " music; this might stop the saver from listening"
          " as well. Winamp with a CD usually counts as "
          "'listening'. Windows Media Player with MP3s "
          "does not.", -1,19,168,206,25
  CONTROL "Try to listen to the music",102,"Button",
           BS_AUTOCHECKBOX | WS_TABSTOP,11,198,105,15
END

To add audio-sampling to your own project:

  1. Link against winmm.lib in your projects settings.
  2. #include <complex>,<list>.
  3. Copy the main body of code from audioin.cpp.
  4. If you want to have the waveform-viewer in your config dialog, you have to RegisterClass("Waveform") in your WinMain, as is done in audioin.cpp.

Note: the FFT runs rather slowly in debug mode. But when you recompile it in release mode, with optimizations turned on, then it's pretty fast enough. Anyway, the fft() will NOT be called if there are messages in the queue, to ensure it doesn't interfere with user responsiveness. Note: FFT needs buf_nsamples>=1024.

Under Visual C++ 6, this code generated an internal compiler error with the default "Release" optimizations. So, I chose "custom optimizations" and turned them all on. Buggy VC++. It was fine under VS.NET and BCB5.

In this article I described only the API for using the audio-in code. To find out how it's implemented, read audioin.cpp. (I'm very proud of this implementation! Please look at it!)

typedef short sample_t;
const int waveid=WAVE_MAPPER, nchannels=2, frequency=22050;
const int nbufs=8, buf_nsamples=1024;  
const int buf_nlog=10, buf_nsqrt=32;   
// nsamples must be a power of 2, at least 1024

// log and sqrt: ie. 2^nlog==nsqrt^2==nsamples

These configuration constants control what the audio will be like. typedef sample_t can be either unsigned char for 8bit audio, or (signed) short for 16bit. Windows doesn't support any other formats. Observe that I make this choice with compile-time typedefs, rather than a runtime variable. This lets the compiler generate optimal code. The constant waveid says which sound source to use, and nchannels, frequency say what format it should be.

The audio system will use nbufs buffers internally: while you're processing one buffer, Windows can be filling up the others with audio. 8 is a good number. Each buffer will contain buf_nsamples samples. Thus, each buffer lasts nsamples/frequency seconds (about 50ms in this case) and your app will be notified about this frequently, via a message.

For the Fourier transform, buf_nlog and buf_nsqrt must be set according to nsamples. Again, these two are compile-time constants for optimal code generation. The Fourier transform code requires that nsamples be at least 1024. If you comment out the FFT code, you can use smaller buffers. That'd let you get more frequent notifications.

void AudioStart(HWND hwnd);
void AudioStop(HWND hwnd);

sample_t *waveformData=wavbuf; 
unsigned char spectrumData[nchannels]; 
// Access the first with waveformData[i*nchannels+chan]

// The second  is the result of an FFT.

// Max frequency in the array is frequency/2

Call AudioStart(hwnd) when a window wants to listen to audio. The window will receive WM_APP messages every time a new audio-sample has been heard. Then call AudioStop(hwnd) when the window is no longer interested. Many windows can call AudioStart(); they will all be notified on each sample. The audio-sampling system is turned on when the first window calls AudioStart, and is turned off when the last window has called AudioStop.

When a window receives WM_APP, the raw waveform audio data is stored in waveformData[i*nchannels+chan] where i ranges between 0 and buf_nsamples. The Fourier data is stored in spectrumData[i][chan], where i again has the same range, but here corresponds to a frequency range from 0Hz ... frequency/2 Hz.

ThreeDee example

This saver uses graphics-hardware acceleration (via OpenGL) to draw a spinning cube. If you want to use the same technique in your own code,

  1. #include <GL gl.h>and <GL glu.h>.
  2. Under the Project Settings > Linker > Input, add opengl32.lib and glu32.lib.
  3. In the part of WinMain which does RegisterClass, OpenGL requires that the saver window has the style CS_OWNDC.
  4. Copy out the block of code in the middle, with functions ChoosePixelFormatEx(), EnsureGL(), EnsureMode() &c.

There are a number of issues. On some systems (especially Win95/98), it can take a long time for 3D graphics drivers to load. This might be unacceptable for the preview in the Display Properties > Screensaver dialog. You might consider not using any 3D graphics in the preview -- instead, maybe just a still image.

There's very little chance of getting 3D acceleration working on multiple monitors. Therefore, when running in full-screen mode, this saver does its 3D thing only on the primary monitor, and leaves the rest blank. If there happened to be an error initializing the 3D graphics stuff, then it leaves the error message bouncing around the screen.

If our 3D window moves, certain hardware graphics cards require to be informed. But consider the case when we have preview window inside the Display Settings dialog. When the user moves the dialog, we never get any notification. Our solution is to GetWindowRect() on every timer tick, and reset the viewport for the graphics card. This is in the function EnsureProj().

We really want hardware acceleration. The normal API function ChoosePixelFormat() isn't as single-minded about hardware acceleration as we'd like. Therefore, we use our own ChoosePixelFormatEx() function, which is.

This saver has an option to change screen resolution for its full-screen mode. There are a couple of issues. We don't change screen mode immediately, but instead wait until our window has drawn itself (so hiding the normal desktop). This is so that, when we change mode, the user doesn't see the existing windows changing. The place we do this is in OnTimer, in response to the first timer tick. This is an example of 'lazy evaluation', where initialization is only done on the first occasion when it's actually needed, not before.

To change screen resolution, you call the function ChangeDisplaySettings(). But on some systems, if you merely specify a width/height/BPP, but without specifying a refresh frequency, then it gives you an ugly low frequency. To avoid this problem, we enumerate all the display modes, and pick the one that has the nicest-looking frequency. This is in the function EnsureMode().

When a mode-change happens, some spurious mouse-move messages get sent by the system. Normally a mouse-move would cause a saver to terminate. We use a sleazy way to avoid this: we set the global flag IsDialogActive, which is normally just used for the password-verify-dialog that appears on screen. Our saver-window WndProc ignores mouse-move messages when it is set. I encourage everyone who use screen-mode-changes to test it with SCRDEBUG set to false, and with mouse sensitivity set to high.

On Win95/98/ME, the saver itself invokes the password-verify dialog. But if we are rendering 3D graphics at the same time as the dialog is up, and we're calling SwapBuffers(), then the password-verify dialog will get ruined. To avoid this, we suspend animation while a dialog is active.

In this article, we focus on the API and how to call it. To see the implementations of the various functions, read opengl.cpp.

int ChoosePixelFormatEx(HDC hdc,int *p_bpp,int *p_depth,
                        int *p_dbl,int *p_acc);
// int bpp=-1, depth=16, dbl=0, acc=1;

// ChoosePixelFormat(hdc,&bpp,&depth,&dbl,&acc);

// Initial values of the variables are -1=don't care,

// 16=desired bpp, 1=turn-on, 0=turn-off

// It chooses the best pixel format, and updates the

// parameters with what was chosen.


void EnsureGL(HDC hdc);
// Question: when is a safe time to create a GL context?

// Answer: Not too early, otherwise it fails. In response

// to the first WM_TIMER is a good place. (You can call this

// function multiple times, eg. every timer tick; it only

// does stuff the first time you call it.)


void ProjGL(const RECT rc);
// This 'prepares the projection', by telling the OpenGL

// system our window's size and screen position and camera

// parameters. This must be called every time the window

// moves on-screen. But we don't. Therefore, call it every

// timer tick just to be on the safe side. (If the window

// hasn't actually moved, then the function does nothing).


void ExitGL();
// Call this when you've finished.

// In ~TSaverWindow (ie. WM_DESTROY) is a good place.


void EnsureMode(HWND hwnd);
// Changes to a 640x480x16bpp resolution. The window 'hwnd'

// should be a topmost window which has already drawn

// itself, so as to blank out the desktop. (You can call

// this function multiple times, eg. every timer tick.

// If the mode has already changed, it does nothing).


void ExitMode();
// Restores screen to normal when you're done.

// In ~TSaverWindow (ie. WM_DESTROY) is a good place.

The following code shows when and how to call the above functions from a piece of saver code.

struct TSaverWindow
{ HWND hwnd; int id;
  HDC hdc;
  // OpenGL requires the CS_OWNDC style. This is it.


  TSaverWindow(HWND h,int i) : hwnd(h),id(i),hdc(0)
  { SetTimer(hwnd,1,50,NULL);
  }

  ~TSaverWindow()
  { KillTimer(hwnd,1); timer=false;
    if (hdc!=0) ReleaseDC(hwnd,hdc); hdc=0;
    if (id==0) {ExitGL(); ExitMode();}
  }

  void OnPaint(HDC hdc,const RECT &rc)
  { // If we're using OpenGL in this window, and it is

    // setup ok, then OnTimer will do a complete redraw.

    // otherwise we do it ourselves.

    if (id==0 && glok &&!IsDialogActive) {OnTimer();return;}
    //

    FillRect(hdc,&rc,(HBRUSH)GetStockObject(BLACK_BRUSH));
    if (id==0 && !IsDialogActive)
    { // If there was an opengl error, we display it.

      int y=(GetTickCount()/20)%rc.bottom, x=y;
      RECT trc; trc.left=x-400; trc.top=y-20;
      trc.right=x+400; trc.bottom=y+20;
      SelectObject(hdc,GetStockObject(DEFAULT_GUI_FONT));
      SetBkColor(hdc,RGB(0,0,0));
      SetTextColor(hdc,RGB(255,128,128));
      DrawText(hdc,glerr.c_str(),-1,&trc,
               DT_CENTER|DT_VCENTER|DT_SINGLELINE);
    }
  }

  void OnTimer()
  { // On secondary monitors, just do normal GDI painting:

    if (id>0) {InvalidateRect(hwnd,NULL,FALSE); return;}
    // If we're the primary monitor, change screen. We do

    // this screen-change lazily, here instead of in the

    // constructor, so that our window has had a change to

    // paint itself (so hiding the desktop) before we do

    // the mode-change.

    if (id==0 && ChangeScreenMode) EnsureMode(hwnd);
    // Our window has CS_OWNDC. 'hdc' is it.

    if (hdc==0) hdc=GetDC(hwnd);
    // EnsureGL can be called multiple times:

    EnsureGL(hdc);
    // We have to reproject ourselves every time our window

    // moves (eg. in the display properties dialog). This

    // can't be done through a WM_MOVE message since, as a

    // child in the display properties, we won't even get

    // a WM_MOVE when the dialog is dragged around.

    RECT rc; GetWindowRect(hwnd,&rc); ProjGL(rc);
    // If opengl didn't work, resort to normal GDI painting

    if (!glok) {InvalidateRect(hwnd,NULL,FALSE); return;}
    // If the password-verify dialog is up (Win95/98/ME

    // only) then we shouldn't be calling SwapBuffers

    // on top of it:

    if (IsDialogActive) return;

    RotX += 5; RotY += 5; RotZ += 5;

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 
    glMatrixMode(GL_MODELVIEW); glPushMatrix();
    // ... all the gl drawing functions go here

    glPopMatrix();
    SwapBuffers(hdc);
  }
};

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