在开发一个基于MFC的对话框应用程序时,尝试在OnPaint()函数中编写一些自定义代码。然而,在第一次运行时,应用程序崩溃了。经过检查,发现问题出在OnPaint()函数上。接下来,需要进行调试。调试OnPaint()函数或类似OnMouseMove()的函数非常困难,因为这些函数的变量值只能通过设置远程调试器来观察,而这需要一些设置和额外的机器。
另一种方便的方法是使用OutputDebugString() API来跟踪特定变量的值,并使用SysInternals的DebugView来监控它。接下来,想说的是,是否曾经想过在GUI应用程序中写入控制台?这是一个好主意,对吧?是的,可以将控制台附加到GUI应用程序中。
可以使用标准的Windows API,如AttachConsole()或AllocConsole(),将控制台附加到应用程序。这两个选项的区别在于,第一个需要一个单独的控制台应用程序,可以将当前进程附加到该应用程序。AttachConsole() API的参数是进程标识符。因此,需要控制台应用程序的PID,以便将其附加。下一个,AllocConsole(),为创建了一个新的控制台并将其附加到进程。它不需要任何参数,也不返回任何值。如果成功,将看到一个新控制台窗口等待输入/输出命令。
到目前为止,一切都很好!下一步是必须向附加的控制台发出输入/输出命令。可以使用GetStdHandle() API检索STDIN和STDOUT的句柄。借助ReadConsole/WriteConsole API,可以从控制台读取或写入。请参见MSDN上的这些API的原型。
BOOL ReadConsole(
HANDLE hConsoleInput,
LPVOID lpBuffer,
DWORD nNumberOfCharsToRead,
LPDWORD lpNumberOfCharsRead,
LPVOID lpReserved
);
BOOL WriteConsole(
HANDLE hConsoleOutput,
CONST VOID *lpBuffer,
DWORD nNumberOfCharsToWrite,
LPDWORD lpNumberOfCharsWritten,
LPVOID lpReserved
);
说实话,不喜欢为每个输入/输出命令安排一长串参数。如果可以使用相同的cin, cout, scanf, printf命令处理控制台,那就更好了。但不幸的是,尽管已经成功分配了一个控制台(使用AttachConsole或AllocConsole),但cin和cout或scanf和printf命令会失败。最好自己试试。如果调试并观察stdin和stout的内容(实际上是&_iobuf[0]和&_iobuf[1]),可以看到两者的_file成员都没有初始化(-1)。它们应该分别是0和1。为了使用标准库的函数,必须使用新分配的控制台的句柄值初始化stdin和stout。借助GetStdHandle(STD_INPUT_HANDLE),可以检索新分配的控制台的标准输入句柄。可以使用_open_osfhandle() API获取STDIN的文件句柄。_fdopen() API返回一个已经打开的流的FILE指针(fpStdIn)。接下来,必须用新的FILE指针替换stdin。
*stdin = fpStdIn;
类似地:
*stdout = fpStdOut;
这对scanf和printf函数有效。但是,如果在分配控制台之前调用了cin或cout,那么分配控制台后的后续调用将失败。解决方案是在分配控制台后立即清空cin和cout。
将讨论的内容封装到一个简单的类ConsoleAdapter中。它主要有以下三个函数:
在创建ConsoleAdapter实例时,可以指定控制台是否需要在实例被删除时自动释放。通过关闭自动删除,必须显式调用DestroyConsole()来销毁控制台。CreateConsole()和SpawnDumpConsole()接受控制台类型作为参数,即可以指定控制台的输入、输出或两者模式(INPUT_CONS, OUTPUT_CONS, BOTH)。
一次只能将一个控制台附加到进程。因此,一次只能使用CreateConsole()或SpawnDumpConsole()中的任何一个。
提供了一个示例转储控制台应用程序,其路径可以作为SpawnDumpConsole()的参数指定。
bool ConsoleAdapter::CreateConsole( CONSOLETYPE_e eConsoleType )
{
try
{
m_eConsoleType = eConsoleType;
AllocConsole();
return ReplaceHandles();
}
catch( ... )
{
return false;
}
}
在CreateProcess()之后需要一个小的延迟。这是因为CreateProcess()在子进程完全初始化之前返回TRUE。
bool ConsoleAdapter::SpawnDumpConsole( LPCTSTR lpctszDumConsoleApp, CONSOLETYPE_e eConsoleType )
{
try
{
m_eConsoleType = eConsoleType;
STARTUPINFO stStartUpInfo = {0};
PROCESS_INFORMATION stProcInfo = {0};
if(!CreateProcess( lpctszDumConsoleApp, 0, 0, 0, TRUE, CREATE_NEW_CONSOLE, 0, 0, &stStartUpInfo, &stProcInfo ))
{
return false;
}
m_hDumpConsole = stProcInfo.hProcess;
// Waiting for the child process to be initialized
Sleep(100);
if(!AttachConsole( stProcInfo.dwProcessId ))
{
return false;
}
ReplaceHandles();
}
catch( ... )
{
return false;
}
return true;
}
提供的示例应用程序展示了如何使用ConsoleAdapter。
选择控制台类型:输入、输出或两者。如果想附加到提供的DumpConsole的控制台,只需浏览DumpConsole.exe的路径并点击“Attach”按钮。将看到一个控制台,如屏幕截图所示。或者,可以通过点击“Create Console”按钮创建一个新控制台。
可以在提供的编辑框中输入要显示在控制台中的消息,位于“Write To Console”按钮的左侧。当点击“Write To Console”按钮时,消息将显示在控制台中。点击“Read From Console”按钮从控制台读取输入字符串(可以读取任何数据类型,但这里正在读取一个字符串)。