Sunday, December 1, 2013

Create A Win32 Console

Creating a Win32 Console
Windows applications can often take advantage of a Win32 Console to provide simple character-mode I/O which is useful for displaying debugging information or log messages, and provides a simple way to input commands for the application.  When you create a console process under Visual Studio, the console is created for you.  This happens when your compiled program entry point is a main function.  If the compiled application is a Win32 application with a WinMain entry function, you must create your own console in order to use one.  It is not difficult to do, and once done, provides a convenient way to work with the stdin, stdout and stderr character streams.

The example code Main.cpp is a typical WinMain function.  The function is very simple.  It creates an application object and tells it to run.  When the application is finished running, the WindMain function exits and your program ends.

Main.cpp

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include "Application.h"


int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
 Application* app = new Application;
 if( app->init(hInstance))
  app->run();
 return 0;
}

The program Application is the master object which creates the functioning parts of the program. It consists of an init function which creates the objects and a shutdown function which deletes the objects. It also has an update function which updates each object.  The run method is an endless loop which continues to repeat as long the update function returns true.  If update returns false, the shutdown function is called and the application exits the run method which returns to WinMain and the program ends.

Application.h

#ifndef __APPLICATION_H__
#define __APPLICATION_H__

#include <windows.h>
#include "Console.h"

class Application
{
public:
 bool init(HINSTANCE hInst);
 void run();
 void shutDown();
 bool update();
 void render();

private:
 void setAppDefaults();

private:
 HINSTANCE m_hInstance;
 Console* con;

private:
};
#endif

For this demo, the application init function creates the application's one and only object; the console.  If console creation is successful, the console will be repeatedly updated by invoking the update function in the application run method.

Application.cpp

#include "Application.h"

bool Application::init(HINSTANCE hInst)
{
 // create application framework
 m_hInstance = hInst;

 // create a console for logging
 con = 0;
 con = new Console;
 if (!con->init())
  return false;
 con->printBanner();

 return true;
}

void Application::shutDown()
{
 if (con) delete con;
}

bool Application::update()
{
 con->update();
 return true;
}

void Application::render()
{
}

void Application::run()
{
 for(;;)
 {
  if ( !update() )
   break;
  render();
 }
 shutDown();
}

Console.h defines the console object, which is really a manager of sorts, responsible for creating the command console, providing methods for interacting with the console and ultimately, cleanly deleting it when it is no longer needed.

Console.h

#ifndef __CONSOLE_H__
#define __CONSOLE_H__

#include <windows.h>
#include <string>
#include <iostream>
#include <stdio.h>


class Console
{
public:
 bool init();
 void shutDown();
 void update();
 void print(char * text, ...);
 void printBanner();

 bool isConsole() {return m_bCons;}

 ~Console() {shutDown();}

protected:
 void command();
    bool errorExit (std::string s);
 void keyEventProc(KEY_EVENT_RECORD ker);
    void mouseEventProc(MOUSE_EVENT_RECORD mer);
 void resizeEventProc(WINDOW_BUFFER_SIZE_RECORD wbsr);

private:
 HWND  m_hCons;    // window handle of console
 bool  m_bCons;    // console created if true
 DWORD  m_dwOldMode;   // old I/O state
    DWORD  m_dwMode;    // current I/O state
 HANDLE  m_hStdIn;    // standard handles
 HANDLE  m_hStdOut;
 HANDLE  m_hStdErr;
 std::string m_CmdBuf;    // buffer for input strings
 FILE*  m_ConOut;    // file stream pointer
};

#endif

The init function creates a new console with Windows AllocConsole function. After this, gathers the standard I/O handles and then redirects stdout and stderr to the console.  The freopen_s function can also redirect stdout or stderr to a file by replacing "CON" with the file specification, such as "app.log".

Console.cpp

#include "console.h"

bool Console::init()
{
 m_bCons = false;

 if ( !AllocConsole() )
  return false;

 m_hStdIn  = GetStdHandle(STD_INPUT_HANDLE);
 m_hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
 m_hStdErr = GetStdHandle(STD_ERROR_HANDLE);

 errno_t err = 0;
 
 err = freopen_s( &m_ConOut, "CON", "w", stdout);
 if (err)
  return errorExit("Redir stdout");
 err = freopen_s( &m_ConOut, "CON", "w", stderr);
 if (err)
  return errorExit("Redir stderr");
 

 //CONSOLE_SCREEN_BUFFER_INFO lpSBI;
 //GetConsoleScreenBufferInfo(m_hStdOut, &lpSBI);
 COORD sz;
 sz.X = 80;
 sz.Y = 1024;
 SetConsoleScreenBufferSize(m_hStdOut, sz);

 if (m_hStdIn == INVALID_HANDLE_VALUE)
        return errorExit((LPSTR)"GetStdHandle");

    m_CmdBuf = "";  // create an empty command buffer

    // Save the current input mode, to be restored on exit.
    if (! GetConsoleMode(m_hStdIn, &m_dwOldMode) )
        return errorExit((LPSTR)"GetConsoleMode");

    m_dwMode = ENABLE_WINDOW_INPUT | ENABLE_MOUSE_INPUT;
    if (! SetConsoleMode(m_hStdIn, m_dwMode) )
        return errorExit((LPSTR)"SetConsoleMode");

 m_hCons = GetConsoleWindow();
 if (m_hCons <= 0)
  return false;
 m_pCons = this;
 return true;
}

Finally, the screen buffer is set to 8 characters by 1024 lines, we get the current input mode, set the console input mode to suit our application, then gather other miscellaneous info about the console we may or may not need later.  The rest of the demo code for the console object is shown below, including the printBanner functions illustrates several different ways, text can be output to the console.

Console.cpp (cont)

bool Console::errorExit(std::string s)
{
 std::cerr<<"**CONSOLE_init FAIL** "<< s << std::endl;
 return false;
}

void Console::print(char *text, ...)
{
 va_list argList;
 va_start(argList, text);

 //Write the text
 vfprintf(stdout, text, argList);
 putc('\n', stdout);

 va_end(argList);
}

void Console::shutDown()
{
 fflush(stdout);
 fflush(stderr);
 ////fclose(m_ConOut);
    SetConsoleMode(m_hStdIn, m_dwOldMode);
 FreeConsole();
}

void Console::update()
{
 // read console events
 //LPDWORD nEvents = 0;
    DWORD nEvents;

    //std::cout << m_hStdIn << std::endl;

 GetNumberOfConsoleInputEvents(m_hStdIn, &nEvents);
 if ( !nEvents )
  return;

 INPUT_RECORD irInBuf[128];
 DWORD   cNumRead;
    if (! ReadConsoleInput(
            m_hStdIn,    // input buffer handle
            irInBuf,     // buffer to read into
            128,         // size of read buffer
            &cNumRead) ) // number of records read
        errorExit("ReadConsoleInput");

    for (DWORD i = 0; i < cNumRead; i++)
    {
        switch(irInBuf[i].EventType)
        {
            case KEY_EVENT: // keyboard input
                keyEventProc(irInBuf[i].Event.KeyEvent);
                break;

            case MOUSE_EVENT: // mouse input
                mouseEventProc(irInBuf[i].Event.MouseEvent);
                break;

            case WINDOW_BUFFER_SIZE_EVENT: // scrn buf. resizing
                resizeEventProc( irInBuf[i].Event.WindowBufferSizeEvent );
                break;

            case FOCUS_EVENT:  // disregard focus events

            case MENU_EVENT:   // disregard menu events
                break;

            default:
                errorExit("Unknown event type");
                break;
        }
    }
}
void Console::keyEventProc(KEY_EVENT_RECORD ker)
{
    char t = '\0';
    if (ker.bKeyDown)
    {
        char t = ker.uChar.AsciiChar;

        if (t == 13)
        {
            std::cout << std::endl;
            // process the command buffer
            command();
            // reset the command buffer
            m_CmdBuf = "";
            return;
        }
        if (t == 8)
        {
            if (!m_CmdBuf.empty())
            {
                m_CmdBuf.resize (m_CmdBuf.size () - 1);
            }
            std::cout<<t;
            std::cout<<" ";
        }
        std::cout<<t;
        if (t>31)
            m_CmdBuf += t;
    }
}

void Console::mouseEventProc(MOUSE_EVENT_RECORD mer)
{
#ifndef MOUSE_HWHEELED
#define MOUSE_HWHEELED 0x0008
#endif
    return;

    printf("Mouse event: ");

    switch(mer.dwEventFlags)
    {
        case 0:

            if (mer.dwButtonState == FROM_LEFT_1ST_BUTTON_PRESSED)
            {
            }
            else if (mer.dwButtonState == RIGHTMOST_BUTTON_PRESSED)
            {
            }
            else
            {
            }
            break;
        case DOUBLE_CLICK:
            break;
        case MOUSE_HWHEELED:
            break;
        case MOUSE_MOVED:
            break;
        case MOUSE_WHEELED:
            break;
        default:
            break;
    }
}

void Console::resizeEventProc(WINDOW_BUFFER_SIZE_RECORD wbsr)
{
}

void Console::printBanner()
{
 printf("+==============================================+\n");
 printf("+           E10 Graphics Framework             +\n");
 printf("+              (c) 2012 - 2013                 +\n");
 printf("+                 J. Kellams                   +\n");
 printf("+==============================================+\n");
 std::cerr << "testing" << std::endl;
}

The command function demonstrates a method for processing user input in the console window.  Entering the command "exit" will close the console window and terminate the application.

void Console::command()
{
 bool result = false;
 
 if (m_CmdBuf == "exit"){
  SendMessage(m_hCons, WM_CLOSE, 0, 0);
  result = true;
 }
 if (result)
  print("Command processed: %s", m_CmdBuf.c_str());
 else
  print("Unknown command: %s", m_CmdBuf.c_str()); 
}


Now, while this code works (or at least appears to) it is far from perfect. In fact, there is a major flaw in the shutdown.  If the user closes the console window from the system menu (clicking the 'X" button, for example) the shutdown method is bypassed and the application is not shutdown in a controlled way.  I will discuss this in more detail in the next post and show some ways to prevent this problem.

No comments:

Post a Comment