Another Way to Handle InputAKA Pointers Are Your Friends Before we begin, to read this article you must know:
In this article I'm going to explain another way to handle input apart from the traditional keys[ ] array. I used to mess up the code with the keys[ ] because I always add new keys or even keyboard modes when I'm coding. One incorrect brace can drive the compiler mad. So, my aim is to make a system where you can just register a key and its function. It will support several keyboard modes (menu mode, game mode, limbo mode, ..). I will also add a function for text editing and a function to scan what number a key has. Source code can be found at the end of this article... with that said... here we go... First, we make a header file: // kbdctrl.h #ifndef kbdctrl #define kbdctrl void KC_init( LRESULT (*)(HWND, UINT , WPARAM ,LPARAM)); void KC_SetMode(int); int KC_GetMode (void); void KC_SetKey(int, void( *)(int, int)); void KC_SetKey(char, void( *)(int, int)); void KC_SetMouse(int, void( *)(int, int)); void KC_Editor(char *, int, bool *); void KC_Scan(int *, bool *); LRESULT KC_Parse(HWND,UINT,WPARAM,LPARAM); #endif Then, the source file. // kbdctrl.cpp #include <windows.h> #include "kbdctrl.h" The principle is simple, but the syntax is not. The base of our system is a two dimensional array of pointers: the first index number is the mode, the second indicates the key the function is bound to, the value is a pointer to the function bound to that key. All the functions must return void and accept two integers as argument. Ill reserve space for 8 modes, 256 keyboard keys and 10 mouse keys. (The keyword static can be omitted). static void ( *event_ptr[8][266])(int, int); As you see, the syntax is rather odd; this is why function pointers are used so seldom. static LRESULT ( *nextproc)(HWND, UINT , WPARAM ,LPARAM) = &DefWindowProc; This variable will hold the pointer to the next Windows message handler. All unprocessed messages will be directed to it. It is DefWindowProc (the default handler) by default. static int curmod = 0; This int holds the number of the current mode (between 0 and 7). static bool editor = false; // Is The Editor Active static char *ed_string; // Pointer To The String Containing The Typed Data static int ed_offset = 0; // Cursor Position, The Index Of The NEXT Character In The String static int ed_length = 0; // Length Of ed_string static bool *ed_done; // Pointer To The Variable That Is Set To True If Enter Is Pressed (And The Editor Ends) These are the global variables used by the editor mode. static bool scan = false; // Is The Scanner Active static bool *sc_done; // Pointer To The Variable Set To True If A Key Has Been Pressed static int *sc_value; // Pointer To The Variable Holding The Value Of The Key For the scan mode. void KC_EXEC(int pos, int x, int y) { if(event_ptr[curmod][pos] != NULL) (*event_ptr[curmod][pos])(x,y); } This little function invokes the pos function in the current mode with parameters x and y if the pointer is not null. If the pointer is null, there is no function and no execution is needed. void KC_init(LRESULT(*next)(HWND, UINT , WPARAM ,LPARAM)) { nextproc = next; } This function is used to register the Windows message handler that can handle the unprocessed messages. void KC_SetMode(int mod) { curmod = mod; } int KC_GetMode (void) { return curmod; } Two functions to get and set the mode. void KC_SetKey(int token, void( *event)(int, int)) { event_ptr[curmod][token] = event; } This function binds a function to a key: token holds the key number and void(*event)(int,int) holds a pointer to a function. void KC_SetKey(char token, void( *event)(int, int)) { event_ptr[curmod][token] = event; } This function is an overloaded version of the former and can be altered if the conversion char-int is incorrect. void KC_SetMouse(int token, void( *event)(int, int)) { // ******************************************** // mouse tokens: +0 double-click // +1 pressed // +2 released // // [0-2] left [3-5] right [6-8] middle // // 9 mouse moved // ******************************************** // No Scrolling Support Yet // ******************************************** event_ptr[curmod][token+256] = event; } This function is used to register a mouse key:
void KC_Editor(char *str, int len, bool *done) { ed_string = str; ed_offset = 0; ed_length = len; ed_done = done; editor = true; } void KC_Scan(int *val, bool *done) { sc_value = val; sc_done = done; scan = true; } These two functions register the values for the editor and scanner and enable them. The use of these functions might need a little explanation. Because we cant wait for the functions to return data (the engine must go on), the data and state are stored in a variable the parent function can access (the pointer is passed, not the value). When the *done is true, the parent function knows the correct value is in the variable (str[ ] and *val) and can take the appropriate steps. Now the big one... LRESULT KC_Parse(HWND hWnd,UINT uMsg,WPARAM wParam,LPARAM lParam) The KC_Parse funtion is to be used as a Windows message handler, this explains the rather odd data types used as return value and arguments. { if(scan) { if(uMsg == WM_KEYDOWN) { *sc_value = wParam; *sc_done = true; scan = false; return 0; } } This is clear, I guess, if we are in scan mode and a key is held down, store the key number and set *sc_done to true, so the parent function knows the number is ready. 0 is returned, so, when in scan mode, there is no other mouse or keyboard input possible. else { if(editor) { If in editor mode... if(uMsg == WM_CHAR) { and a WM_CHAR message is received (this message contains a character, not a key number, so it can be used like that). if(wParam == VK_RETURN) { editor = false; *ed_done = true; return 0; } If enter is pressed, end editor mode, and tell the parent the string is ready (AND return null). if((wParam == VK_BACK) && (ed_offset != 0) ) { ed_string[--ed_offset] = NULL; return 0; } If backspace is pressed and the cursor is not on the 0th position, set the previous character in the string to null and point the offset(cursor position) to it. (AND return 0). if(ed_offset < ed_length) { ed_string[ed_offset++] = wParam; } If the string isnt full (offset < length and not offset return 0; } } return 0, so, that if in editor mode, a character message ends the function. The KEYDOWN message (that always comes with the character message) is not processed because of the else statement on the next line. This, in short, means that all keyboard commands are blocked, BUT, mouse input is still processed. else { switch (uMsg) { case WM_KEYDOWN: { KC_EXEC(wParam,1,0); // Send 1,0 return 0; } case WM_KEYUP: { KC_EXEC(wParam,0,0); // Send 0,0 return 0; } }; } So, when we arent in edit mode, keyboard input is processed. A KEYDOWN message executes the function corresponding with the key code, NOT to the character code (they often match, but not always). The function gets 1,0 as parameters. If a KEYUP is received, the parameters are 0,0. switch (uMsg) // Check For Windows Messages { case WM_LBUTTONDBLCLK: { KC_EXEC(256,LOWORD(lParam),HIWORD(lParam)); return 0; } case WM_LBUTTONDOWN: { KC_EXEC(257,LOWORD(lParam),HIWORD(lParam)); return 0; } case WM_LBUTTONUP: { KC_EXEC(258,LOWORD(lParam),HIWORD(lParam)); return 0; } case WM_MBUTTONDBLCLK: { KC_EXEC(259,LOWORD(lParam),HIWORD(lParam)); return 0; } case WM_MBUTTONDOWN: { KC_EXEC(260,LOWORD(lParam),HIWORD(lParam)); return 0; } case WM_MBUTTONUP: { KC_EXEC(261,LOWORD(lParam),HIWORD(lParam)); return 0; } case WM_RBUTTONDBLCLK: { KC_EXEC(262,LOWORD(lParam),HIWORD(lParam)); return 0; } case WM_RBUTTONDOWN: { KC_EXEC(263,LOWORD(lParam),HIWORD(lParam)); return 0; } case WM_RBUTTONUP: { KC_EXEC(264,LOWORD(lParam),HIWORD(lParam)); return 0; } case WM_MOUSEMOVE: { KC_EXEC(265,LOWORD(lParam),HIWORD(lParam)); return 0; } The mouse code. If a message is received, the corresponding function is executed. The parameters are the x and y position of the mouse relative to the upper left corner of the window. (for the LOWORD and HIGHWORD stuff, please refer to MSDN). }; } return ( *nextproc)(hWnd,uMsg,wParam,lParam); } The unprocessed messages are sent to the next handler (DefWindowProc by default). So, to make the use clear, I will implement a NeHe program using some misc functions, and full 3D movement. It, however, is only a test suit I used for tracing bugs, so it isnt exactly esthetical (if any one wants to make something more fancy, please do so and let me know). #include <stdlib.h> #include <stdio.h> #include <windows.h> // Header File For Windows #include <stdio.h> // Header File For Standard Input/Output #include <gl\gl.h> // Header File For The OpenGL32 Library #include <gl\glu.h> // Header File For The GLu32 Library #include <gl\glaux.h> // Header File For The Glaux Library #include <math.h> #include "kbdctrl.h" #define STRIDE 2 // Length Of One pace #define piover180 0.01745329252f GLYPHMETRICSFLOAT gmf[96]; GLuint fontbase; Variables for 3D movement. GLfloat xpos,ypos,zpos; GLfloat xrot, yrot; bool done; bool active = TRUE; // Window Active Flag Set To TRUE By Default bool fullscreen = TRUE; // Fullscreen Flag Set To Fullscreen Mode By Default GLfloat modif = 2.0f; Mouse sensitivity (the smaller the more sensitive). bool test = false; Enable or disable the test data output. LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // Declaration For WndProc void func1 (int, int); // When F1 Is Pressed, This Function Is Needed GLvoid BuildFont(GLvoid) // Build Our Bitmap Font { HFONT font; // Windows Font ID HFONT oldfont; // Used For Good House Keeping fontbase = glGenLists(96); // Storage For 96 Characters ( NEW ) font = CreateFont( -10, 0, // Width Of Font 0, // Angle Of Escapement 0, // Orientation Angle FW_BOLD, // Font Weight FALSE, // Italic FALSE, // Underline FALSE, // Strikeout ANSI_CHARSET, // Character Set Identifier OUT_TT_PRECIS, // Output Precision CLIP_DEFAULT_PRECIS, // Clipping Precision ANTIALIASED_QUALITY, // Output Quality FF_DONTCARE|DEFAULT_PITCH, // Family And Pitch "Courier New"); // Font Name oldfont = (HFONT)SelectObject(hDC, font); // Selects The Font We Want wglUseFontOutlines(hDC, 32, 96,fontbase,0.0f,0.0f,WGL_FONT_POLYGONS,gmf); SelectObject(hDC, oldfont); // Selects The Font We Want DeleteObject(font); // Delete The Font } GLvoid KillFont(GLvoid) // Delete The Font List { glDeleteLists(fontbase, 96); // Delete All 96 Characters ( NEW ) } The buildfont and killfont functions, they are explained in lesson 13 and 14 of the NeHe Tuts. GLvoid glPrint(GLfloat xpos, GLfloat ypos, GLfloat zpos, GLfloat scale, char *string) { An adapted glPrint function, with scaling (making the font bigger or smaller). glPushAttrib(GL_LIST_BIT); // Pushes The Display List Bits glPushMatrix(); // Pushes The Matrix glListBase(fontbase - 32); // Sets The Base Character to 32 glTranslatef(xpos,ypos,zpos); glScalef(scale,scale,1.0f); glCallLists(strlen(string), GL_UNSIGNED_BYTE, string); // Draws The Display List Text glPopMatrix(); // Pops The Matrix glPopAttrib(); // Pops The Display List Bits } This should be clear. void mousemove (int x, int y) { xrot += (x-320)/modif; yrot += (y-240)/modif; } The simplest mouse function there is, unfortunately, to simple to work void straferight (int x, int y) { GLfloat cosx = cos(xrot * piover180); xpos += STRIDE * cos(yrot * piover180) * cosx; zpos += STRIDE * sin(yrot * piover180) * cosx; } void strafeleft (int x, int y) { GLfloat cosx = cos(xrot * piover180); xpos -= STRIDE * cos(yrot * piover180) * cosx; zpos -= STRIDE * sin(yrot * piover180) * cosx; } Functions for strafe left and right, and no y movement (for the math, it isnt to hard to find that out, so, take a piece of paper and get it done) void downmove(int x, int y) { GLfloat cosx = cos(xrot * piover180); xpos += STRIDE * sin(yrot * piover180) * cosx; ypos -= STRIDE * sin(xrot * piover180); zpos += STRIDE * cos(yrot * piover180) * cosx; } void upmove(int x, int y) { GLfloat cosx = cos(xrot * piover180); xpos -= STRIDE * sin(yrot * piover180) * cosx; ypos += STRIDE * sin(xrot * piover180); zpos -= STRIDE * cos(yrot * piover180) * cosx; } Moving forward and backward void MO_mousemove(int x, int y) { This is a more decent mouse function. static int MO_x, MO_y; The keyword static might need some explanation: if a variable is declared static, it is allocated statically, so each time you call this function, the value is still there from the last run. The only way of removing it from memory is program termination. It has all the properties of a global variable, except that it is only visible in this function. The variable hold the position the cursor had when the function was called the last time. yrot += (MO_x - x) /modif; xrot += (MO_y - y) /modif; The x and y movements are calculated relative to the last time the function was used and dived by the speed modifier. if(x < 20 || x > 620 || y < 20 || y > 460) { MO_x = 320; MO_y = 240; SetCursorPos(320,240); } If the cursor approaches on of the borders of the window, reset it to the center. else { MO_x = x; MO_y = y; } } If not, store the current position for the next run. void stop(int x, int y) { done=TRUE; // End The Main Loop } void AN_act(int x, int y) { KC_SetMode(1); // Go To Mode 2 } void act(int x, int y) { ShowCursor(true); // Show The Cursor KC_SetMode(0); // Go To Mode 0 } void modif_up(int x, int y) { modif += 0.1f; // Lower Mouse Speed } void modif_down(int x, int y) { modif -= 0.1f; // Raise Mouse Speed } void MO_act(int x, int y) { ShowCursor(false); // Hide Cursor KC_SetMode(2); // Go To Mode 2 } void reset(int x, int y) // Reset Movement { xrot = 0.0f; yrot = 0.0f; xpos = 0.0f; ypos = 0.0f; zpos = 0.0f; } Some more functions to be bound to the keys, most of them are self-explanatory. void oneshot(int x, int y) { static bool active = false; if(!active && x == 1) { active = true; test=!test; } if(x == 0) { active = false; } } This is the one shot function, it is used to switch (holding the button only triggers it once). Once more, I use a static variable and not a global. It is not, like it might seem, set to false each time the function is triggered, but only when the program is started. void ED_act(int x, int y) { This one is tricky, it has its own main loop, so seems to stop the engine and go on on its own. KC_SetMode(3); The mode is set to three, (an empty one, so no input is processed except the edit line) bool ED_done = false; char ED_string [30]; The two variables need by the editor: the string and the status. memset (ED_string,NULL,30); Fill the string with 0 (otherwise it might do weird things). KC_Editor(ED_string,30,&ED_done); Start the editor. MSG msg; while(!ED_done) // Loop That Runs While done=FALSE { if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)) // Is There A Message Waiting? { if (msg.message==WM_QUIT) // Have We Received A Quit Message? { done=TRUE; // If So done=TRUE } else // If Not, Deal With Window Messages { TranslateMessage(&msg); // Translate The Message DispatchMessage(&msg); // Dispatch The Message } } else // If There Are No Messages { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity(); glPrint(0.0f,0.0f,-20.0f,0.5f, ED_string); SwapBuffers(hDC); // Swap Buffers (Double Buffering) } } KC_SetMode(2); } The special main loop, going as long as the editor hasnt given an OK and printing out the string (see tutorial 1). GLvoid StartKC(GLvoid) { KC_SetMode(0); // Mode Is Set To 0, Mode With Crappy Mouse Movement KC_SetKey(VK_F1, &func1); // Register F1 KC_SetKey(VK_UP, &upmove); KC_SetKey(VK_DOWN, &downmove); KC_SetKey(VK_LEFT, &strafeleft); KC_SetKey(VK_RIGHT, &straferight); KC_SetMouse(9, &mousemove); // Register Mouse Movement KC_SetKey(VK_ESCAPE, &stop); // Register Escape KC_SetKey('A', &act); // Register A Key To Mode 0 KC_SetKey('Z', &AN_act); // Register Z Key To Mode 1 KC_SetKey('E', &MO_act); // Register E Key To Mode 3 KC_SetKey('R', &reset); // Register R Key To Reset KC_SetKey('T', &oneshot); // Register T Key To The Switch Function KC_SetKey('Y', &ED_act); // Register Y Key To The Editor (And The Special Main Loop) KC_SetMode(1); // Mode Is Set To 1, Beter Mouse Movement KC_SetKey(VK_F1, &func1); KC_SetKey(VK_UP, &upmove); KC_SetKey(VK_DOWN, &downmove); KC_SetKey(VK_LEFT, &strafeleft); KC_SetKey(VK_RIGHT, &straferight); KC_SetMouse(9, &MO_mousemove); KC_SetKey(VK_ESCAPE, &stop); KC_SetKey('A', &act); KC_SetKey('Z', &AN_act); KC_SetKey('E', &MO_act); KC_SetKey('R', &reset); KC_SetKey('T', &oneshot); KC_SetMode(2); // Mode Is Set To Two, Up And Down Are Now Used For Altering Scroll Speed KC_SetKey(VK_F1, &func1); KC_SetKey(VK_UP, &modif_up); KC_SetKey(VK_DOWN, &modif_down); KC_SetMouse(9, &MO_mousemove); KC_SetKey(VK_ESCAPE, &stop); KC_SetKey('A', &act); KC_SetKey('Z', &AN_act); KC_SetKey('E', &MO_act); KC_SetKey('R', &reset); KC_SetKey('T', &oneshot); // Mode 3 Is Empty To Block Mouse Input In Editor Mode } Registration of the keys and their functions. int InitGL(GLvoid) // All Setup For OpenGL Goes Here { glEnable(GL_TEXTURE_2D); // Enable Texture Mapping ( NEW ) glShadeModel(GL_SMOOTH); // Enable Smooth Shading glClearColor(0.0f, 0.0f, 0.0f, 0.5f); // Black Background glClearDepth(1.0f); // Depth Buffer Setup glEnable(GL_DEPTH_TEST); // Enables Depth Testing glDepthFunc(GL_LEQUAL); // The Type Of Depth Testing To Do glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); // Really Nice Perspective Calculations StartKC(); BuildFont(); return TRUE; // Initialization Went OK } Two lines must be inserted: StartKC(); and BuildFont(); int DrawGLScene(GLvoid) // Here's Where We Do All The Drawing { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Clear The Screen And The Depth Buffer glLoadIdentity(); // Reset The View Clear the buffers and reset view. if(test) { char str[50]; sprintf(str, "%d %f ", KC_GetMode(), modif); glPrint(0.0f,0.0f,-10.0f,0.5f, str); sprintf(str, "%f %f", xrot, yrot); glPrint(-2.0f,-2.0f,-10.0f,0.5f, str); } If test mode is on, print out the mode and mouse modification variable an under it the x and y rotation. glRotatef(-xrot,1.0,0.0,0.0); glRotatef(-yrot,0.0,1.0,0.0); glTranslatef(-xpos, -ypos, -zpos); Transformations for 3D movement (all following code should move correct). glBegin(GL_LINES); glColor3f(1.0f,0.0f,0.0f); glVertex3f(-10.0f, 0.0f , 0.0f); glColor3f(0.0f,1.0f,0.0f); glVertex3f(10.0f, 0.0f , 0.0f); glColor3f(0.0f,1.0f,0.0f); glVertex3f(0.0f, -10.0f , 0.0f); glColor3f(0.0f,0.0f,1.0f); glVertex3f(0.0f, 10.0f , 0.0f); glColor3f(1.0f,0.0f,1.0f); glVertex3f(0.0f, 0.0f , -10.0f); glColor3f(1.0f,1.0f,0.0f); glVertex3f(0.0f, 0.0f , 10.0f); glEnd(); A very simple cross indicating the axis. glColor3f(1.0f,0.0f,0.0f); glPrint(0.0f,0.0f,0.0f,5.0f, "help"); glPrint(0.0f,1.0f,12.0f,5.0f, "help2"); Two times the word help, to see if you are moving upward down or not. return TRUE; // Keep Going } On the end of the KillGLWindow, the line. KillFont(); Must be inserted. void func1 (int x, int y) { static bool active = false; if(!active && x == 1) { active = true; KillGLWindow(); // Kill Our Current Window fullscreen=!fullscreen; // Toggle Fullscreen / Windowed Mode // Recreate Our OpenGL Window CreateGLWindow("NeHe's Texture Mapping Tutorial",640,480,16,fullscreen) } if(x == 0) { active = false; } } A switch function for the F1 key. LRESULT CALLBACK WndProc( HWND hWnd, // Handle For This Window UINT uMsg, // Message For This Window WPARAM wParam, // Additional Message Information LPARAM lParam) // Additional Message Information { switch (uMsg) // Check For Windows Messages { case WM_ACTIVATE: // Watch For Window Activate Message { if (!HIWORD(wParam)) // Check Minimization State { active=TRUE; // Program Is Active } else { active=FALSE; // Program Is No Longer Active } return 0; // Return To The Message Loop } case WM_SYSCOMMAND: // Intercept System Commands { switch (wParam) // Check System Calls { case SC_SCREENSAVE: // Screensaver Trying To Start? case SC_MONITORPOWER: // Monitor Trying To Enter Powersave? return 0; // Prevent From Happening } break; // Exit } case WM_CLOSE: // Did We Receive A Close Message? { PostQuitMessage(0); // Send A Quit Message return 0; // Jump Back } case WM_SIZE: // Resize The OpenGL Window { // LoWord=Width, HiWord=Height ReSizeGLScene(LOWORD(lParam),HIWORD(lParam)); return 0; // Jump Back } } // Pass All Unhandled Messages To DefWindowProc return KC_Parse(hWnd,uMsg,wParam,lParam); } the altered WndProc, passing all unprocessed message to the parser int WINAPI WinMain( HINSTANCE hInstance, // Instance HINSTANCE hPrevInstance, // Previous Instance LPSTR lpCmdLine, // Command Line Parameters int nCmdShow) // Window Show State { MSG msg; // Windows Message Structure done=FALSE; // Bool Variable To Exit Loop // Ask The User Which Screen Mode They Prefer if (MessageBox(NULL,"Would You Like To Run In Fullscreen Mode?", "Start FullScreen?",MB_YESNO|MB_ICONQUESTION)==IDNO) { fullscreen=FALSE; // Windowed Mode } // Create Our OpenGL Window if (!CreateGLWindow("NeHe's Texture Mapping Tutorial",640,480,16,fullscreen)) { return 0; // Quit If Window Was Not Created } while(!done) // Loop That Runs While done=FALSE { if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)) // Is There A Message Waiting? { if (msg.message==WM_QUIT) // Have We Received A Quit Message? { done=TRUE; // If So done=TRUE } else // If Not, Deal With Window Messages { TranslateMessage(&msg); // Translate The Message DispatchMessage(&msg); // Dispatch The Message } } else // If There Are No Messages { // Draw The Scene. Watch For ESC Key And Quit Messages From DrawGLScene() if ((active && !DrawGLScene())) // Active? Was There A Quit Received? { done=TRUE; // ESC or DrawGLScene Signalled A Quit } else // Not Time To Quit, Update Screen { SwapBuffers(hDC); // Swap Buffers (Double Buffering) } } } // Shutdown KillGLWindow(); // Kill The Window return (msg.wParam); // Exit The Program } The WinMain, no longer handling the keyboard. So, to end, I would like to discuss some obvious modifications of the code: -adepting the number of modes: just changing the array were it was declared. PS. The edit command always prints the key character as first character. This can be avoided in two ways. If you will never trigger the edit function with the mouse, the initial offset can be set to 1 and a new filter added before the if(ed_offset < ed_length) if(ed_offset < 0) { ed_offset++; return 0; } The other solution is using postmessage(hWnd, WM_CHAR, VK_RETURN,0); after starting the editor. * Download Code For This Article |