MFC 对话框应用程序中的自定义代码调试

在开发一个基于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中。它主要有以下三个函数:

  • CreateConsole - 使用AllocConsole() API分配一个新的控制台,并使用ReplaceHandles()函数替换std句柄。
  • SpawnDumpConsole - 附加到指定参数的新转储控制台;提供了一个示例虚拟控制台实现。
  • DestroyConsole - 使用FreeConsole() API分离控制台。一旦调用此函数,后续的输入/输出调用将失败。在SpawnConsole()创建的控制台的情况下,将终止生成的控制台应用程序。

在创建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。

使用ConsoleAdapter演示

选择控制台类型:输入、输出或两者。如果想附加到提供的DumpConsole的控制台,只需浏览DumpConsole.exe的路径并点击“Attach”按钮。将看到一个控制台,如屏幕截图所示。或者,可以通过点击“Create Console”按钮创建一个新控制台。

可以在提供的编辑框中输入要显示在控制台中的消息,位于“Write To Console”按钮的左侧。当点击“Write To Console”按钮时,消息将显示在控制台中。点击“Read From Console”按钮从控制台读取输入字符串(可以读取任何数据类型,但这里正在读取一个字符串)。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485