Function graphics in 3D

Environment: VC6, NT4 SP3

This is a simple Windows application, which creates and reproduces 3D-image (surface) of a function plot using OpenGL rendering context.
Graphics data may be arbitrary (specified by the user). Data format becomes obvious if you consider (look up the codes of) the function COGView::DefaultGraphic.
The application makes it convenient to inspect the results of calculation of some field (for example: surfaces of equal temperature,
pressure velocity or magnetic flux density in 3D space).
For instance, The View-class may be incorporated in an application which deals with the PDE-solutions.
By mouse manipulations you can control the graphics position and rotation.
Modeless dialog allows to control some other OpenGL image attributes.

Start with the MFC AppWizard SDI template named OG. Place the following include-directives at the end of StdAfx.h file in order to ensure the visibility of
OpenGL and STL library resources.

#include < afxdlgs.h >
#include < math.h >

#include < gl/gl.h >
#include < gl/glu.h >

#include < vector >
using namespace std;

Invent the data structure

We shall need a class incapsulating the functionality of a point in 3D space (let’s call it CPoint3D).
The container of its objects (vector) will be used as a storage of the surface vertices.
Place the following class declaration at the beginning of OGView.h file.
Simplify the starting code of the View-class interface deleting some members and insert the new members in it.

class CPoint3D
float x, y, z;
CPoint3D () { x=y=z=0; }
CPoint3D (float c1, float c2, float c3)
x = c1; z = c2; y = c3;
CPoint3D& operator=(const CPoint3D& pt)
x = pt.x; z = pt.z; y = pt.y;
return *this;
CPoint3D (const CPoint3D& pt)
*this = pt;

class COGView : public CView
//======== New data
long m_BkClr; // Background color
HGLRC m_hRC; // Rendering context OpenGL
HDC m_hdc; // Windows device context
GLfloat m_AngleX; // Rotation angle (around X-axis)
GLfloat m_AngleY; // Rotation angle (around Y-axis)
GLfloat m_AngleView; // Perspective angle
GLfloat m_fRangeX; // Graphics dimension (along X-axis)
GLfloat m_fRangeY; // Graphics dimension (along Y-axis)
GLfloat m_fRangeZ; // Graphics dimension (along Z-axis)
GLfloat m_dx; // Displacement quantum (along X-axis)
GLfloat m_dy; // Displacement quantum (along Y-axis)
GLfloat m_xTrans; // Displacement (along X-axis)
GLfloat m_yTrans; // Displacement (along Y-axis)
GLfloat m_zTrans; // Displacement (along Z-axis)
GLenum m_FillMode; // Polygon filling mode
bool m_bCaptured; // Mouse capture flag
bool m_bRightButton; // Right mouse button flag
bool m_bQuad; // Flag of using GL_QUAD (instead of GL_QUAD_STRIP)
CPoint m_pt; // Current mouse position
UINT m_xSize; // Current client window sixe (along X-axis)
UINT m_zSize; // Current client window sixe (along -axis)
vector < CPoint3D > m_cPoints; // Graphics dimension (along X-axis)
int m_LightParam[11]; // Graphics dimension (along X-axis)
CPropDlg *m_pDlg; // Graphics dimension (along X-axis)

//======== Public methods
COGDoc* GetDocument()
{ return DYNAMIC_DOWNCAST(COGDoc,m_pDocument); }
virtual ~COGView();

//======== New methods
void DrawScene(); // Prepare and store the image
void DefaultGraphic(); // Create and save the default plot
void ReadData(); // Data-file manipulations
bool DoRead(HANDLE hFile); // Data-file reading
//===== Take the data from buffer and store in m_cPoints
void SetGraphPoints(BYTE* buff, DWORD nSize);
void SetLightParam (short lp, int nPos); // Set lighting parameters
void GetLightParams(int *pPos); // Get lighting parameters
void SetLight(); // Set the lighting
void SetBkColor(); // Set background color
void LimitAngles(); // Limit rotation angles

virtual void OnDraw(CDC* pDC);
virtual BOOL PreCreateWindow(CREATESTRUCT& cs);

Add handlers

Using ClassWizard insert in COGView responses (in other words, add message handlers) to the following Windows messages:
Change the View-class constructor to initialize controlled parameters.

//====== Rendering context does not exist yet
m_hRC = 0;

//====== Initial image turn
m_AngleX = 35.f;
m_AngleY = 20.f;

//====== Projection matrix view angle
m_AngleView = 45.f;

//====== Initial bkcolor
m_BkClr = RGB(0, 0, 96);

// Initial mode to fill the inner polygons (quads) points
m_FillMode = GL_FILL;

//====== Initial plot creation

//====== Initial image shift
//====== One and a half object size (backward)
m_zTrans = -1.5f*m_fRangeX;
m_xTrans = m_yTrans = 0.f;

//== Initial shift quantums (for rotation animation)
m_dx = m_dy = 0.f;

//====== Mouse is not captured
m_bCaptured = false;
//====== Right mouse button has not been pressed
m_bRightButton = false;
//====== We use quads to create the surface
m_bQuad = true;

//====== Initial lighting params
m_LightParam[0] = 50; // X position
m_LightParam[1] = 80; // Y position
m_LightParam[2] = 100; // Z position
m_LightParam[3] = 15; // Ambient light
m_LightParam[4] = 70; // Diffuse light
m_LightParam[5] = 100; // Specular light
m_LightParam[6] = 100; // Ambient material
m_LightParam[7] = 100; // Diffuse material
m_LightParam[8] = 40; // Specular material
m_LightParam[9] = 70; // Shininess material
m_LightParam[10] = 0; // Emission material

Prepare the OpenGL context

You possibly know from the multiple articles (available here at CodeGuru or at Silicon Graphics that to prepare a window for drawing with OpenGL commands you should:

  • select and set the pixel format,
  • get and store Windows device context,
  • create and store OpenGL rendering context,
  • add the window styles WS_CLIPSIBLINGS | WS_CLIPCHILDREN,
  • refuse background erasure before each redrawing (handle WM_ERASEBKGND and just return TRUE),
  • specifically handle the WM_SIZE, WM_PAINT messages and
  • release contexts when the view is closing.

If you frequently use OpenGL you had better remember all these steps and perform them as a kind of ritual.
The main events in preparing a window for OpenGL usage take place in WM_CREATE handler.

int COGView::OnCreate(LPCREATESTRUCT lpCreateStruct)
if (CView::OnCreate(lpCreateStruct) == -1)
return -1;

// Structure used to describe the format
1, // Version
PFD_DOUBLEBUFFER, // Use double buffering
// (more efficient drawing)
PFD_TYPE_RGBA, // No pallettes
24, // Number of color planes
// in each color buffer
24, 0, // for Red-component
24, 0, // for Green-component
24, 0, // for Blue-component
24, 0, // for Alpha-component
0, // Number of planes
// of Accumulation buffer
0, // for Red-component
0, // for Green-component
0, // for Blue-component
0, // for Alpha-component
32, // Depth of Z-buffer
0, // Depth of Stencil-buffer
0, // Depth of Auxiliary-buffer
0, // Now is ignored
0, // Number of planes
0, // Now is ignored
0, // Color of transparent mask
0 // Now is ignored

//====== Get current Windows context
m_hdc = ::GetDC(GetSafeHwnd());

//====== Ask to find the nearest compatible pixel-format
int iD = ChoosePixelFormat(m_hdc, &pfd);
if ( !iD )
return -1;

//====== Try to set this format
if ( !SetPixelFormat (m_hdc, iD, &pfd) )
return -1;

//====== Try to create the OpenGL rendering context
if ( !(m_hRC = wglCreateContext (m_hdc)))
return -1;

//====== Try to put it in action
if ( !wglMakeCurrent (m_hdc, m_hRC))
return -1;

//====== Now you can issue OpenGL commands
// (i.e. call gl*** functions)
glEnable(GL_LIGHTING); // Lighting will be used
//====== Only one (first) source of light
//====== Depth of the Z-buffer will be taken into account
//====== Material colors will be taken into account

//====== Our function to set the background

//====== Create and store the image in a special list
// of OpenGL commands
return 0;


Each time you (or Windows) decide to redraw the view, the same chain of actions is performed and it should be given in WM_PAINT handler.
Considering the peculiarities of the (admired by Microsoft) Document-View architecture you place the redrawing code not in the OnPaint but in OnDraw function.

Here we erase back-buffer (image in memory) and depth-buffer,
correct the modeling matrix (which transforms the image), call the stored list of drawing commands
and after (re) drawing in the memory switch front and back buffers to quickly show the result on the screen.

void COGView:: OnDraw(CDC* pDC)
//========== Clear the background and Z-buffer
//========== Clean the modeling matrix
// (make it equal the unity matrix)

//======= At first set the light
// (otherwise it will rotate with the image)

//====== Change the modeling matrix coefficients in order
glTranslatef(m_xTrans,m_yTrans,m_zTrans); // to shift
glRotatef (m_AngleX, 1.0f, 0.0f, 0.0f ); // and to rotate
glRotatef (m_AngleY, 0.0f, 1.0f, 0.0f );

//====== the following vertices coordinates
// (they are being multiplied by matrix)

//====== Switch back and front buffers
// (to show what happened)

The lighting

If you want to know how OpenGL calculates the color of each pixel when it processes lighting equation,
you should find the book “OpenGL Programming Guide (Addison-Wesley Publishing Company)”. Try to do it at
Here we simply use the technique gathered there.

void COGView::SetLight()
//====== Both surface sides are considered when calculating
//====== each pixel color with the lighting formula


//====== Light source position depends on the
// object sizes scaled to (0,100)

float fPos[] =
glLightfv(GL_LIGHT0, GL_POSITION, fPos);

//====== Ambient light intensity
float f = m_LightParam[3]/100.f;
float fAmbient[4] = { f, f, f, 0.f };
glLightfv(GL_LIGHT0, GL_AMBIENT, fAmbient);

//====== Diffuse light intensity
f = m_LightParam[4]/100.f;
float fDiffuse[4] = { f, f, f, 0.f };
glLightfv(GL_LIGHT0, GL_DIFFUSE, fDiffuse);

//====== Specular light intensity
f = m_LightParam[5]/100.f;
float fSpecular[4] = { f, f, f, 0.f };
glLightfv(GL_LIGHT0, GL_SPECULAR, fSpecular);

//====== Surface material reflection properties for
// each light component
f = m_LightParam[6]/100.f;
float fAmbMat[4] = { f, f, f, 0.f };
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, fAmbMat);

f = m_LightParam[7]/100.f;
float fDifMat[4] = { f, f, f, 1.f };
glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, fDifMat);

f = m_LightParam[8]/100.f;
float fSpecMat[4] = { f, f, f, 0.f };
glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, fSpecMat);

//====== Material shininess
float fShine = 128 * m_LightParam[9]/100.f;

//====== Material light emission property
f = m_LightParam[10]/100.f;
float fEmission[4] = { f, f, f, 0.f };
glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, fEmission);

Setting the background color is easy compared to other OpenGL intricacies.

void COGView::SetBkColor()
//====== Split the color to 3 components
GLclampf red = GetRValue(m_BkClr)/255.f,
green = GetGValue(m_BkClr)/255.f,
blue = GetBValue(m_BkClr)/255.f;
//====== Set the clear (background) color
glClearColor (red, green, blue, 0.f);

//====== Actual background erasure

Creating and storing the image

The main job on generating the image is done in DrawScene function that creates and stores the image consisting of graphics primitives vertices’ coordinates.
We assume that all these coordinates are already placed in STL-container m_cPoints. The image is constructed
either as a series of curved quadrangles (GL_QUADS) or as a series of connected curved quads (GL_QUAD_STRIP).
Later we shall add the command to toggle between the two modes.
The surface points are actually over the regular coordinate grid in (X, Z) plane. The size of this grid is stored in
m_xSize and m_zSize variables. In spite of the 2-dimensional character of the grid we use linear (one-dimensional)
array m_cPoints or (to be exact) container of type vector for storing the coordinates’ values.
This is to save efforts when executing file operations.

The selection of 4 neighboring points of one primitive being generated (GL_QUADS for example) is done with the help of 4 indices (n, i, j, k).
The index n goes in succession through all the vertices in order from left to right along the X-axis (Z=0) and then the procedure is repeated (gradually increasing the Z-value).
Other indices are computed relative to n-index. Note that only two indices work in connected quads mode which is enabled or disabled by the m_bQuad flag.

void COGView::DrawScene()
//====== Create the new list of OpenGL commands
glNewList(1, GL_COMPILE);

//====== Set the polygon filling mode
glPolygonMode(GL_FRONT_AND_BACK, m_FillMode);

//====== Image grid sizes
UINT nx = m_xSize-1,
nz = m_zSize-1;

//====== Turn on the primitive connection mode
// (not connected)
if (m_bQuad)
glBegin (GL_QUADS);

for (UINT z=0, i=0; z < nz; z++, i++) { //====== Turn on the primitive connection mode // (connected) //====== The strip of connected quads begins here if (!m_bQuad) glBegin(GL_QUAD_STRIP); for (UINT x=0; x < nx; x++, i++) { // i, j, k, n 4 indices of a quad // Counter Clockwise direction int j = i + m_xSize, // Other vertices indices k = j+1, n = i+1; //=== Get coordinates of 4 vertices float xi = m_cPoints[i].x, yi = m_cPoints[i].y, zi = m_cPoints[i].z, xj = m_cPoints[j].x, yj = m_cPoints[j].y, zj = m_cPoints[j].z, xk = m_cPoints[k].x, yk = m_cPoints[k].y, zk = m_cPoints[k].z, xn = m_cPoints[n].x, yn = m_cPoints[n].y, zn = m_cPoints[n].z, //=== Quad side lines vectors coordinates ax = xi-xn, ay = yi-yn, by = yj-yi, bz = zj-zi, //====== Normal vector coordinates vx = ay*bz, vy = -bz*ax, vz = ax*by, //====== Normal vector length v = float(sqrt(vx*vx + vy*vy + vz*vz)); //====== Scale to unity vx /= v; vy /= v; vz /= v; //====== Set the normal vector glNormal3f (vx,vy,vz); //===== Not connected quads branch if (m_bQuad) { //====== Vertices are given in counter clockwise // direction order glColor3f (0.2f, 0.8f, 1.f); glVertex3f (xi, yi, zi); glColor3f (0.6f, 0.7f, 1.f); glVertex3f (xj, yj, zj); glColor3f (0.7f, 0.9f, 1.f); glVertex3f (xk, yk, zk); glColor3f (0.7f, 0.8f, 1.f); glVertex3f (xn, yn, zn); } else //===== Connected quads branch { glColor3f (0.9f, 0.9f, 1.0f); glVertex3f (xi, yi, zi); glColor3f (0.5f, 0.8f, 1.0f); glVertex3f (xj, yj, zj); } } //====== Close block of GL_QUAD_STRIP commands if (!m_bQuad) glEnd(); } //====== Close block of GL_QUADS commands if (m_bQuad) glEnd(); //====== Close the list of OpenGL commands glEndList(); }

Normal vectors should be correctly calculated and specified for each primitive. If you are lazy to do this job accurately you will never get the quality of lighting and (as a result)
the quality of the whole image.

File manipulations

Container m_cPoints is filled when the data is read from a file. We want to have a default surface plot and
for this reason we must create a file with some sample data and read it when the application is first opened.
We are going to work with the binary file and save the information in our own fomat wich can be described verbally.
First the grid dimensions (m_xSize and m_zSize) are placed, then the function y = f (x, z) values are successively placed in a data file.
Before writing to a file all the data are placed in a temporary BYTE (i.e. unsigned char) buffer.
The fact that variables of different types are placed in a binary file complicate a bit the code but simplifies actual
reading and writing which are done with only one API-function call.

void COGView::DefaultGraphic()
//====== Coordinate grid dimensions
m_xSize = m_zSize = 33;

//====== Number of meshes is less than number of nodes
UINT nz = m_zSize – 1,
nx = m_xSize – 1;

// File size in bytes
DWORD nSize =
m_xSize * m_zSize * sizeof(float) + 2*sizeof(UINT);

//====== Temporary buffer
BYTE *buff = new BYTE[nSize+1];

//====== Point at the start of it with UINT-type pointer
UINT *p = (UINT*)buff;

//====== Place the two UINTs
*p++ = m_xSize;
*p++ = m_zSize;

//====== Change the pointer type to continue with the
// floating numbers
float *pf = (float*)p;

//=== Default formula coefficients
double fi = atan(1.)*6,
kx = fi/nx,
kz = fi/nz;

//====== For all the grid nodes calculate
//=== calculate and place default function values
// in the same buffer
for (UINT i=0; i < m_zSize; i++) { for (UINT j=0; j < m_xSize; j++) { //====== Sample function *pf++ = float (sin(kz*(i-nz/2.)) * sin(kx*(j-nx/2.))); } } //=== We want to know the real number of bytes written // to the file DWORD nBytes; //=== Create and open the default data file (sin.dat) HANDLE hFile = CreateFile(_T("sin.dat"), GENERIC_WRITE, 0,0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0); //====== Write the whole buffer WriteFile(hFile,(LPCVOID)buff, nSize,&nBytes, 0); //====== Close the file CloseHandle(hFile); //====== Create and fill container m_cPoints // (using the same buffer) SetGraphPoints (buff, nSize); //====== Free temporary buffer delete [] buff; }

Reading the data

The next function ReadData creates the file dialog which enables the user to select a data file, reads the data and creates the image.
The standard file dialog uses the OFN_EXPLORER style which works right only in Windows 2000.

void COGView::ReadData()
//=== Here we place the file path
TCHAR szFile[MAX_PATH] = { 0 };

//====== File extensions filter
TCHAR *szFilter =TEXT(“Graphics Data Files (*.dat)\0”)
TEXT(“All Files\0”)

//====== Query the current folder

//=== Struct used by the standard file dialog

//====== Dialog parameters
ofn.lStructSize = sizeof(OPENFILENAME);
//====== Window which owns the dialog
ofn.hwndOwner = GetSafeHwnd();
ofn.lpstrFilter = szFilter;
//====== The filters string index (begins with 1)
ofn.nFilterIndex = 1;
ofn.lpstrFile = szFile;
ofn.nMaxFile = sizeof(szFile);
//====== Dialog caption
ofn.lpstrTitle = _T(“Find a data file”);
ofn.nMaxFileTitle = sizeof (ofn.lpstrTitle);
//====== Dialog style (only in Win2K)
ofn.Flags = OFN_EXPLORER;

//====== Create and open the dialog (retuns 0 on failure)
if (GetOpenFileName(&ofn))
// Try to open the file (which must exist)
HANDLE hFile = CreateFile(ofn.lpstrFile, GENERIC_READ,

//=== On failure CreateFile returns -1
if (hFile == (HANDLE)-1)
MessageBox(_T(“Could not open this file”));

//====== Try to read the data
if (!DoRead(hFile))

//====== Create and store the image

//====== Redraw the window client region

The TEXT macros sequence is equivalent to one string

TEXT(“Graphics Data Files (*.dat)\0*.dat\0All Files\0*.*\0”);

due to the fact that C++ compiler glues the literals, created by the TEXT macro.
The zero-code symbols separate filter substrings.
The actual reading is done in DoRead member function where we query the file size,
allocate the needed amount of storage and read the whole file in a storage buffer.
After that we set the graphic point container m_cPoints within the function SetGraphPoints.

bool COGView::DoRead(HANDLE hFile)
//===== Query the file size
DWORD nSize = GetFileSize (hFile, 0);

if (nSize == 0xFFFFFFFF)
MessageBox(_T(“Could not get file size”));
return false;

//===== Try to allocate the core
BYTE *buff = new BYTE[nSize+1];

if (!buff)
MessageBox(_T(“The data file is too big”));
return false;

DWORD nBytes;
//===== Read the whole file in the buffer
ReadFile (hFile, buff, nSize, &nBytes, 0);

if (nSize != nBytes)
MessageBox(_T(“Error while reading data file”));
return false;

//===== Set the vector of vertices coordinates
SetGraphPoints (buff, nSize);

delete [] buff;
return true;

void COGView::SetGraphPoints(BYTE* buff, DWORD nSize)
//===== Here again we use the technique of changing
// the the pointer types
UINT *p = (UINT*)buff;

//==== First read the grid dimensions
m_xSize = *p;
m_zSize = *++p;

//===== Check the file size (just in case)
if (m_xSize<2 || m_zSize < 2 || m_xSize*m_zSize*sizeof(float) + 2 * sizeof(UINT) != nSize) { MessageBox(_T("Wrong data format")); return; } //===== Resize the container m_cPoints.resize(m_xSize*m_zSize); if (m_cPoints.empty()) { MessageBox(_T("Can not allocate the data")); return; } //====== Change the pointer type float x, z, *pf = (float*)++p, fMinY = *pf, fMaxY = *pf, right = (m_xSize-1)/2.f, left = -right, rear = (m_zSize-1)/2.f, front = -rear, range = (right + rear)/2.f; UINT i, j, n; m_fRangeY = range; m_fRangeX = float(m_xSize); m_fRangeZ = float(m_zSize); //===== How far should we place the whole image m_zTrans = -1.5f * m_fRangeZ; //===== Fill the container with 3D poits for (z=front, i=0, n=0; i < m_zSize; i++, z+=1.f) { for (x=left, j=0; j < m_xSize; j++, x+=1.f, n++) { MinMax (*pf, fMinY, fMaxY); m_cPoints[n] = CPoint3D(x,z,*pf++); } } //===== Scale the data so that any data would be seen "nicely" float zoom = fMaxY > fMinY ? range/(fMaxY-fMinY) : 1.f;

for (n=0; n < m_xSize*m_zSize; n++) { m_cPoints[n].y = zoom * (m_cPoints[n].y - fMinY) - range/2.f; } }

Mouse manipulations

To control the orientation and rotation speed of the image with the help of mouse buttons add the following message handlers.
Left button controls two rotation angles and swtches on and off the automatic rotation mode. If the user (additionally) presses the Ctrl button
then he might shift (translate) the image along two (X and Y) axes. Note that horizontal mouse movement should change the rotation angle around the vertical axis and vice versa. The right button is used to control the Z-axis translation. i.e. the distance to the image.
To switch the constant rotation we use some sensativity treshold.

void COGView::LimitAngles()
while (m_AngleX < -360.f) m_AngleX += 360.f; while (m_AngleX > 360.f)
m_AngleX -= 360.f;
while (m_AngleY < -360.f) m_AngleY += 360.f; while (m_AngleY > 360.f)
m_AngleY -= 360.f;

void COGView::OnLButtonDown(UINT nFlags, CPoint point)
//====== Stop rotation

//====== Zero the quantums
m_dx = 0.f;
m_dy = 0.f;

//====== Capture the mouse messages and direct them
// in our window
//====== Remember the fact
m_bCaptured = true;
//====== and where it happened
m_pt = point;

void COGView::OnRButtonDown(UINT nFlags, CPoint point)
//====== Remember the fact
m_bRightButton = true;

//====== and reproduce the left button response
OnLButtonDown(nFlags, point);

void COGView::OnLButtonUp(UINT nFlags, CPoint point)
//====== If we captured the mouse,
if (m_bCaptured)
//=== query the desired quantum value
//=== if it exeeds the sensativity threshold
if (fabs(m_dx) > 0.5f || fabs(m_dy) > 0.5f)
//=== Turn on the constant rotation
//=== Turn off the constant rotation

//====== Clear the capture flag
m_bCaptured = false;

void COGView::OnRButtonUp(UINT nFlags, CPoint point)
m_bRightButton = m_bCaptured = false;

void COGView::OnMouseMove(UINT nFlags, CPoint point)
if (m_bCaptured)
// Desired rotation speed components
m_dy = float(point.y – m_pt.y)/40.f;
m_dx = float(point.x – m_pt.x)/40.f;

//====== If Ctrl was pressed
if (nFlags & MK_CONTROL)
//=== we shift (translate) the image
m_xTrans += m_dx;
m_yTrans -= m_dy;
//====== If the right mouse button is pressed
if (m_bRightButton)
//====== we shift along the z-axis
m_zTrans += (m_dx + m_dy)/2.f;
//====== otherwise we rotate the image
double a = fabs(m_AngleX);
if (90. < a && a < 270.) m_dx = -m_dx; m_AngleX += m_dy; m_AngleY += m_dx; } } //=== In any case we should store the coordinates m_pt = point; Invalidate(FALSE); } } void COGView::OnTimer(UINT nIDEvent) { LimitAngles(); //====== Increase the angles m_AngleX += m_dy; m_AngleY += m_dx; Invalidate(FALSE); }

To control the lighting parameters we supplied the modeless dialog (see downloads) which uses one OnHScroll message handler to handle all the eleven sliders.
That is also a known and rather efficient technology widely used in SDK programming.
The auxiliary GetSliderNum function (and IDs[] array) are needed to eliminate the frequent and unpleasant errors encountered due to resource indices change.


Download demo project – 19 Kb

Download source – 30 Kb

More by Author

Must Read