An EGI Player in C++

Skip to main content (skip navigation menu)






An EGI Player in C++

 

The purpose of this paper is to show you how to build a simple animation player with EGI. It is not the shortest animation player, for that see "The Smallest FLIC animation player", but the EGIplay utility developed here is not a toy example either.

I am building the program in C++, but without any object library (such as MFC). The structure of the program will be familiar to MFC programmers, however.

The complete source code for this application note can be downloaded from a link at the end of this article.

WinMain

The implementation of WinMain is "boilerplate" code. The function registers a window class if there is no previous instance, creates the main window and enters a message loop. As an aside, in 32-bit mode the hPrevInstance parameter is always NULL.

WinMain() and support functions
static char const szAppName[] = "EGIplay";

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                   LPSTR /*lpszCmdParam*/, int nCmdShow)
{
  if (hPrevInstance == NULL && !InitApplication(hInstance))
    return FALSE;

  if (!InitInstance(hInstance, nCmdShow))
    return FALSE;

  MSG Msg;
  while (GetMessage(&Msg, NULL, 0, 0)) {
    TranslateMessage(&Msg);
    DispatchMessage( &Msg );
  } /* while */
  return Msg.wParam;
}

BOOL InitApplication(HINSTANCE hInstance)
{
  WNDCLASS wc = { 0 };
  wc.lpfnWndProc = WndProc;
  wc.hInstance = hInstance;
  wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
  wc.hCursor = LoadCursor(NULL, IDC_ARROW);
  wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
  wc.lpszClassName = szAppName;
  wc.lpszMenuName = szAppName;
  return (RegisterClass(&wc) != 0);
}

HWND InitInstance(HINSTANCE hInstance, int nCmdShow)
{
  HWND hwnd = CreateWindow(szAppName, szAppName,
                           WS_OVERLAPPEDWINDOW,
                           CW_USEDEFAULT, CW_USEDEFAULT,
                           CW_USEDEFAULT, CW_USEDEFAULT,
                           NULL, NULL, hInstance, NULL );
  if (hwnd != NULL) {
    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);
  } /* if */
  return hwnd;
}

The InitApplication function assigns a menu to the window class. This menu is in the resource file for the EGIplay utility, which is below in its entirety:

EGIplay resource file
#include <windows.h>
#include "egiplay.h"

EGIplay MENU
BEGIN
    POPUP    "&File"
    BEGIN
        MENUITEM "&Open...",            IDM_OPEN
        MENUITEM "&Close",              IDM_CLOSE
        MENUITEM SEPARATOR
        MENUITEM "E&xit\tAlt+F4",       IDM_EXIT
    END
    MENUITEM "&About...",               IDM_ABOUT
END

The Window function

My implementation of the window function, WndProc, uses "message crackers" and it is thereby also mostly boilerplate code. I initially need to intercept a handful of messages.

WndProc() - the window function
LRESULT CALLBACK WndProc(HWND hwnd, UINT Message,
                         WPARAM wParam, LPARAM lParam )
{
  switch (Message) {
    HANDLE_MSG(hwnd, WM_COMMAND,         Cls_OnCommand);
    HANDLE_MSG(hwnd, WM_DESTROY,         Cls_OnDestroy);
    HANDLE_MSG(hwnd, WM_PAINT,           Cls_OnPaint);
    HANDLE_MSG(hwnd, WM_PALETTECHANGED,  Cls_OnPaletteChanged);
    HANDLE_MSG(hwnd, WM_QUERYNEWPALETTE, Cls_OnQueryNewPalette);
  default:
    return DefWindowProc(hwnd, Message, wParam, lParam);
  } /* switch */
}

Obviously, the more interesting code is in the Cls_OnCommand(), Cls_OnPaint and the other event functions. The function Cls_OnDestroy() is boilerplate code too (it calls PostQuitMessage() to drop out of the message loop) and I will not discuss it further.

Cls_OnCommand()

The Cls_OnCommand function handles all menu options. To better explain the general operation, I handle each menu options separately. The general framework of the Cls_OnCommand function is:

Cls_OnCommand() framework
static FlicAnim Anim;

void Cls_OnCommand(HWND hwnd, int id, HWND /*hwndCtl*/, UINT /*codeNotify*/)
{
  switch (id) {

    <cases all menu options>

  } /* switch */
}

I have declared a global variable Anim for the animation file. Several menu commands refer to this variable and the event functions access it too.

To start with a very simple menu command, the one of IDM_EXIT is:

Cls_OnCommand() / IDM_EXIT
  case IDM_EXIT:
    DestroyWindow(hwnd);
    break;

When the user selects the "Open" option from the menu, the program pops up a file open dialog and, on success, opens the FLIC file and immediately starts playing it. The code specific to EGI, in the snippet below, is in bold face:

Cls_OnCommand() / IDM_OPEN
  case IDM_OPEN:
    { /* local */
      OPENFILENAME ofn = { 0 };
      char Filename[MAX_PATH] = "";
      ofn.lStructSize  = sizeof ofn;
      ofn.hwndOwner    = hwnd;
      ofn.lpstrFilter  = "FLIC files\0*.flc;*.fli;*.flx\0";
      ofn.nFilterIndex = 1;
      ofn.lpstrFile    = Filename;
      ofn.nMaxFile     = sizeof Filename;
      ofn.Flags        = OFN_FILEMUSTEXIST;
      if (GetOpenFileName(&ofn)) {
        Anim.Open(Filename);
        if (Anim)
          Anim.Play(hwnd);
        else
          MessageBox(hwnd,
                     "Unable to open the requested file (probably due to an "
                     "invalid or unsupported file format).",
                     szAppName, MB_OK | MB_ICONERROR);
      } /* if */
    } /* local */
    break;

Closing an animation file is then a simple matter of:

Cls_OnCommand() / IDM_CLOSE
  case IDM_CLOSE:
    if (Anim)
      Anim.Close();

Cls_OnPaint()

The animation player automatically refreshes the parts of the image that change on each next frame, and bypasses Cls_OnPaint(). The Cls_OnPaint() function is only called on a WM_PAINT event that originates from Microsoft Windows. Below is the implementation of the function, with the code specific to EGI in bold face:

Cls_OnPaint()
void Cls_OnPaint(HWND hwnd)
{
  PAINTSTRUCT PaintStruct;
  BeginPaint(hwnd, &PaintStruct);
  if (Anim)
    Anim.Paint(PaintStruct.hdc);
  EndPaint(hwnd, &PaintStruct);
}

Cls_OnPaletteChanged() and Cls_OnQueryNewPalette()

When the display is set up in a 256-colour mode, you must handle palette messages. The functioning of the Microsoft Windows Palette Manager is a full article on its own. In brief, when Cls_OnQueryNewPalette() is called, Windows gives the program a chance to change the system palette (fully or partially) by selecting its own, optimal, palette. If the system palette actually changes, pixels all over the display may suddenly "jump" to a new (and usually completely wrong) colour. It is the task of the Cls_OnPaletteChanged() function to adjust or repaint the contents of the image to the new system palette.

The Cls_OnPaletteChanged() function is actually quite simple, because the FlicAnim::Paint() method handles the mapping of the animation palette to the system palette, so all that the function has to do is enforce a call to Cls_OnPaint(). The "guard" on the window handle (hwndPaletteChange) is needed for reasons best explained in the paper "The Microsoft Windows Palette Manager".

Cls_OnPaletteChanged()
void Cls_OnPaletteChanged(HWND hwnd, HWND hwndPaletteChange)
{
  if (hwnd != hwndPaletteChange)
    InvalidateRect(hwnd, NULL, TRUE);
}

Cls_OnQueryNewPalette() gets a chance to change the system palette by selecting its own palette. There are a few steps involved in this, because Windows does not pass in a "display context" in which to select the palette and because Cls_OnQueryNewPalette() also has to undo all changes in the state that it sets up. Cls_OnQueryNewPalette() must:

On top of the list above, it is also good measure to return a non-zero value if the selection of the application-specific palette changed any entries in the system palette.

As can be expected, most of the code for Cls_OnQueryNewPalette() is boilerplate code. The code specific to EGI (in the snippet below) is in bold face. For good measure, I test whether an animation is currently open, and skip all of the steps if not:

Cls_OnQueryNewPalette()
BOOL Cls_OnQueryNewPalette(HWND hwnd)
{
  UINT result = 0;

  if (Anim) {
    /* get the palette from the animation */
    HPALETTE hpal = Anim.GetHPalette();

    /* select and realize this palette */
    HDC hdc = GetDC(hwnd);
    hpal = SelectPalette(hdc, hpal, FALSE);
    result = RealizePalette(hdc);

    /* restore the palette (before releasing the DC) */
    SelectPalette(hdc, hpal, TRUE);
    RealizePalette(hdc);
    ReleaseDC(hwnd, hdc);
  } /* if */

  return (result > 0);
}

Extending the animation player

The player developed so far has rudimentary functionality: opening, playing and closing files. Extending it with more simple playback functionality follows the same scheme: add items to the menu and add associated cases to the Cls_OnCommand() function that call methods from the FlicAnim class.

As an example, I will add "Start" and "Stop" menu items to the player.

The first step is to change the resource file. The new items inserted are in bold face:

EGIplay resource file, adapted for new menu commands
#include <windows.h>
#include "egiplay.h"

EGIplay MENU
BEGIN
    POPUP    "&File"
    BEGIN
        MENUITEM "&Open...",            IDM_OPEN
        MENUITEM "&Close",              IDM_CLOSE
        MENUITEM SEPARATOR
        MENUITEM "E&xit\tAlt+F4",       IDM_EXIT
    END
    POPUP    "&Play"
    BEGIN
        MENUITEM "&Start",              IDM_START
        MENUITEM "S&top",               IDM_STOP
    END
    MENUITEM "&About...",               IDM_ABOUT
END

The added cases for Cls_OnCommand() are below:

Cls_OnCommand() / IDM_START & IDM_STO
  case IDM_START:
    if (Anim && !Anim.IsPlaying())
      Anim.Play(hwnd);
    break;

  case IDM_STOP:
    if (Anim) {
      Anim.Stop();
      InvalidateRect(hwnd, NULL, FALSE);
    } /* if */
    break;

The EGI animation player prefetches one frame to ensure best animation fluency. The side effect is that when you stop the animation, one frame may still be in the internal buffer of the animation player. To synchronize the internal frame buffer with the one that is displayed at the screen, you must call the FlicAnim::Paint() method or, as is done here, route this call through the Cls_OnPaint() function.

As an aside, if you cannot live with the animation jumping one frame further when you click "Stop", you must turn the prefetch functionality off. To do this, call FlicAnim::SetParam() with the code FLIC_PARAM_PRELOAD and value 0 (zero).

Of course, for a decent user interface, I should also "gray out" the "Play" menu option if the animation is already playing and gray out the "Stop" option if the animation is stopped. In this example, the "Stop" and "Play" menu options act more like "Pause" and "Resume", because they do not reset the animation to the beginning. If you want the "Play" item to always start from the first frame, call the method FlicAnim::Reset() before calling FlicAnim::Play().

To make the player slightly more interesting, I now want the player to show the first frame after opening, but not to start playing it automatically. It is easy enough to not call FlicAnim::Play() after FlicAnim::Open(), but displaying the first frame is a different matter. As explained in the EGI manual, calling FlicAnim::Paint(), either directly or indirectly through InvalidateRect(), right after FlicAnim::Open() does not display the first frame of the animation. FlicAnim::Open() opened the animation file, but it did not read in the first frame. Function FlicAnim::Paint() draws the current frame in a display context, but right after the call to FlicAnim::Open() there is no current frame yet and FlicAnim::Paint() returns immediately.

The solution, as the manual explains, is to call FlicAnim::NextFrame() to read the first frame, and then cause the window contents to be repainted. Below is the new snippet for the IDM_OPEN case (of the Cls_OnCommand() function), with the changes from the original version in bold face:

Cls_OnCommand() / IDM_OPEN
  case IDM_OPEN:
    { /* local */
      OPENFILENAME ofn = { 0 };
      char Filename[MAX_PATH] = "";
      ofn.lStructSize  = sizeof ofn;
      ofn.hwndOwner    = hwnd;
      ofn.lpstrFilter  = "FLIC files\0*.flc;*.fli;*.flx\0";
      ofn.nFilterIndex = 1;
      ofn.lpstrFile    = Filename;
      ofn.nMaxFile     = sizeof Filename;
      ofn.Flags        = OFN_FILEMUSTEXIST;
      if (GetOpenFileName(&ofn)) {
        Anim.Open(Filename);
        if (Anim) {
          Anim.NextFrame();
          InvalidateRect(hwnd, NULL, FALSE);
        } else
          MessageBox(hwnd,
                     "Unable to open the requested file (probably due to an "
                     "invalid or unsupported file format).",
                     szAppName, MB_OK | MB_ICONERROR);
      } /* if */
    } /* local */
    break;

Compiling and linking

The player developed so far has little requirements and/or dependencies on a particular compiler. Specifically, it does not use MFC or another object library. As a result, compiling it is fairly simple. With the source code for this sample, you will find three batch files:

Batch file Use for
build_vc.bat    For Microsoft Visual C/C++ 6.0 or higher
build_bcc.bat    For Borland C++ 5.x or or Borland C++ Builder
build_wcl.bat    For Watcom C/C++ 11.0 or OpenWatcom

If you are using another C++ compiler than the ones listed, you are probably able to adapt one of these batch files to suit your compiler. As you may verify, the batch files contain quite simple commands. For example, the batch file for Microsoft Visual C/C++ contains the instructions:

rc egiplay.rc
cl -I..\..\include egiplay.cpp egiplay.res ..\..\lib\eplay32m.lib

End notes

This concludes the simple animation player utility. It can be turned into a more professionally looking application by adding options for scaling the animation, sizing the window to the animation, adjusting playback speed, and enabling automation scripts (to name a few). Other application notes go into various specific features in quite a bit more detail. Specifically, a follow-up on this paper is the application note Easy Transparent Animations with the EGI Player. And there is the EGI manual, of course.

Downloads

To run this example, you also need the development files of the EGI animation engine. The EGI evaluation version will do fine.