如何处理从我的 GUI 应用程序启动的控制台窗口?

Posted

技术标签:

【中文标题】如何处理从我的 GUI 应用程序启动的控制台窗口?【英文标题】:How to get handle of a console window launched from my GUI application? 【发布时间】:2012-11-27 13:52:09 【问题描述】:

在我的 GUI 应用程序中,我运行控制台应用程序并需要处理其窗口。我尝试使用 EnumWindows(),请参见下面的代码,但它不起作用。列表中没有我的控制台应用程序。

type
  TEnumWindowsData = record
    ProcessId: Cardinal;
    WinHandle: THandle;
    List: TStrings;                 // For test only
  end;
  PEnumWindowsData = ^TEnumWindowsData;

function FindWindow(hWnd: THandle; lParam: LPARAM): BOOL; stdcall;
var
  ParamData: TEnumWindowsData;
  ProcessId: Cardinal;
  WinTitle: array[0..200] of Char;  // For test only
begin
  ParamData := PEnumWindowsData(lParam)^;
  GetWindowThreadProcessId(hWnd, ProcessId);
  if ProcessId <> ParamData.ProcessId then
    Result := True
  else begin
    ParamData.WinHandle := hWnd;
    Result := False;
  end;
  // For test only
  GetWindowText(hWnd, WinTitle, Length(WinTitle) - 1);
  ParamData.List.Add(IntToStr(ProcessId) + ' ' + IntToStr(hWnd) + ' ' + WinTitle);
end;

procedure TForm1.Button1Click(Sender: TObject);

  function RunApp(const AProgram: string): Cardinal;
  var
    StartupInfo: TStartupInfo;
    ProcessInformation: TProcessInformation;
  begin
    Result := 0;
    ...
    if CreateProcess(nil, PChar(AProgram), nil, nil, False,
          NORMAL_PRIORITY_CLASS, nil, nil, StartupInfo, ProcessInformation)
    then
      Result := ProcessInformation.dwProcessId;
    ...
  end;

var
  ParamData: TEnumWindowsData;
begin
  ParamData.ProcessId := RunApp('cmd.exe /C D:\TMP\TEST.exe');
  ParamData.WinHandle := 0;
  ParamData.List := Memo1.Lines;
  EnumWindows(@FindWindow, THandle(@ParamData));

  FWindowHandle := ParamData.WinHandle;
end;

【问题讨论】:

@Tlama - 谢谢,太好了!在CreateProcess 之后使用Sleep(50) 对我有用。 布兰科,这就是我不喜欢的。我会尝试找到更可靠的东西并在此处发布结果。 @TLama - 我想问你为什么删除,对我来说,很好的解决方案 (AttachConsole(PID), GetConsoleWindow, FreeConsole)。为什么你关心Sleep() 几毫秒? Branko,我已经完成了......但是,对于那个等待问题,我找不到任何可靠的东西。但我想说,这可能是足够的解决方法(至少比依赖Sleep 的固定时间段更好;-) 我猜根本问题是控制台窗口归conhost.exe所有,而不是你创建的进程cmd.exe。 【参考方案1】:

以下代码只是创建进程(控制台应用程序),通过AttachConsole 函数将您的进程附加到新创建的控制台,并从附加的控制台使用GetConsoleWindow 函数获取窗口句柄。

以下代码最大的弱点是CreateProcess函数在此时立即返回,此时控制台还没有完全初始化,当你尝试附加控制台后,你会失败。不幸的是,没有WaitForInputIdle 函数for console applications,所以作为一种可能的解决方法,我会选择尝试在有限的循环计数中附加控制台,一旦成功,获取句柄并分离控制台。

在可能如下所示的代码中。那里的RunApp 函数应该返回控制台窗口的句柄(假设您将只从中运行控制台应用程序),并且应该等待大约。 1 秒用于您已开始可附加的控制台应用程序。您可以通过更改Attempt 变量的初始值和/或更改Sleep 间隔来修改此值。

function GetConsoleWindow: HWND; stdcall;
  external kernel32 name 'GetConsoleWindow';
function AttachConsole(dwProcessId: DWORD): BOOL; stdcall;
  external kernel32 name 'AttachConsole';

function RunApp(const ACmdLine: string): HWND;
var
  CmdLine: string;
  Attempt: Integer;
  StartupInfo: TStartupInfo;
  ProcessInfo: TProcessInformation;
begin
  Result := 0;
  FillChar(StartupInfo, SizeOf(TStartupInfo), 0);
  FillChar(ProcessInfo, SizeOf(TProcessInformation), 0);
  StartupInfo.cb := SizeOf(TStartupInfo);
  StartupInfo.dwFlags := STARTF_USESHOWWINDOW;
  StartupInfo.wShowWindow := SW_SHOWNORMAL;
  CmdLine := ACmdLine;
  UniqueString(CmdLine);
  if CreateProcess(nil, PChar(CmdLine), nil, nil, False,
    CREATE_NEW_CONSOLE, nil, nil, StartupInfo, ProcessInfo) then
  begin
    Attempt := 100;
    while (Attempt > 0) do
    begin
      if AttachConsole(ProcessInfo.dwProcessId) then
      begin
        Result := GetConsoleWindow;
        FreeConsole;
        Break;
      end;
      Sleep(10);
      Dec(Attempt);
    end;
    CloseHandle(ProcessInfo.hThread);
    CloseHandle(ProcessInfo.hProcess);
  end;
end;

然后你可以例如以这种方式更改已启动应用程序的控制台窗口的标题:

procedure TForm1.Button1Click(Sender: TObject);
var
  S: string;
  ConsoleHandle: HWND;
begin
  ConsoleHandle := RunApp('cmd.exe');
  if ConsoleHandle <> 0 then
  begin
    S := 'Hello! I''m your console, how can I serve ?';
    SendTextMessage(ConsoleHandle, WM_SETTEXT, 0, S);
  end;
end;

【讨论】:

谢谢,很遗憾,我无法将两个答案标记为已接受。也感谢你更好、更清楚地提出了我的问题。这是我们英语不好的问题。 我必须接受你的回答 因为它间接解释了我的代码为什么不起作用。我在CreateProcess 之后立即枚举了窗口 - 但后来是 Bummi,在单击按钮后,这就是他的代码始终有效的原因。如果我将Sleep(50) 放在CreateProcess 之后,我的代码也可以工作:) 可能关闭ProcessInfo.hProcessProcessInfo.hThread 句柄? @LURD,这就是我尝试过的。即使您在创建进程后立即关闭它们,除非您等待一段时间,否则您仍然无法附加控制台。我一直在互联网上搜索更可靠的东西,但找不到任何东西。这个循环至少比固定的Sleep 更安全。【参考方案2】:

-创建控制台进程

-找到进程的窗口

-设置找到的控制台窗口的标题

-写入找到的控制台

unit Unit3;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    PID: DWORD;
  public
     Public-Deklarationen 
  end;

var
  Form1: TForm1;

implementation

$R *.dfm

procedure TForm1.Button1Click(Sender: TObject);
var
  AProgram: String;
  StartupInfo: TStartupInfoW;
  ProcessInfo: TProcessInformation;
begin
  AProgram := 'cmd /K Dir C:\temp';   // using /K for keeping console alive
  UniqueString(AProgram);             // ensure that AProgram is writeable by API
  ZeroMemory(@StartupInfo, SizeOf(StartupInfo)); // create minimum startup info
  StartupInfo.cb := SizeOf(StartupInfo);
  StartupInfo.dwFlags := STARTF_USESHOWWINDOW;
  StartupInfo.wShowWindow := SW_SHOW;
  if CreateProcess(nil, PChar(AProgram), nil, nil, False,  // Create Consoleprocess
    CREATE_NEW_CONSOLE or NORMAL_PRIORITY_CLASS, nil, nil, StartupInfo,
    ProcessInfo) then
    try
      PID := ProcessInfo.dwProcessId;  // Store  ProcessId to PID
    finally
      // close not longer required handles
      Showmessage(IntToStr(Integer(CloseHandle(ProcessInfo.hProcess))));
      Showmessage(IntToStr(Integer(CloseHandle(ProcessInfo.hThread))));
    end;
end;

type
  PEnumInfo = ^TEnumInfo;
  TEnumInfo = record ProcessID: DWORD; HWND: THandle; end;

function EnumWindowsProc(Wnd: DWORD; var EI: TEnumInfo): BOOL; stdcall;
var
  PID: DWORD;
begin
  GetWindowThreadProcessID(Wnd, @PID);  // get processID from WND of Enumeration
  // continue EnumWindowsProc if found PID is not our wished, visible and enabled  processID (EI.ProcessID)
  Result := (PID <> EI.ProcessID) or (not IsWindowVisible(WND)) or
    (not IsWindowEnabled(WND));
  if not Result then // WND found for EI.ProcessID
    EI.HWND := WND;
end;

function FindMainWindow(PID: DWORD): DWORD;
var
  EI: TEnumInfo;
begin
  //Store our processID and invalid Windowhandle to EI
  EI.ProcessID := PID;
  EI.HWND := 0;
  EnumWindows(@EnumWindowsProc, Integer(@EI));
  Result := EI.HWND;
end;

function AttachConsole(dwProcessId: DWORD): BOOL; stdcall;
  external kernel32 name 'AttachConsole';

procedure TForm1.Button2Click(Sender: TObject);
var
  Wnd: HWND;
  S: String;
begin
  if PID <> 0 then  // do we have a valid ProcessID
  begin
    Wnd := FindMainWindow(PID);
    if Wnd <> 0 then  // did we find the window handle
    begin
      S := 'Test';
      // change caption of found window
      SendMessage(Wnd, WM_SETTEXT, 0, LPARAM(@S[1]));
      if AttachConsole(PID) then   // are we able to attach to console of our proecess?
      begin
        Writeln('Here we are'); // write to attached console
        FreeConsole;  // free if not longer needed
      end;
    end;
  end;
end;

end.

【讨论】:

谢谢,您的代码运行良好!!我必须看看我的代码有什么问题:) 如果有更多关于它在做什么以及为什么要这样写的解释性文字,我会投票赞成。 @TLama 感谢您的改进。 Marjan Venema 我稍后会添加一些信息。我现在很着急,对不起... @bummi - 抱歉,但我不得不将 TLama 的答案标记为已接受。请阅读我对他的回答的评论。当然,我的感谢仍然存在:) 更好的人必须获胜,这就是生活;-)。恕我直言,还有另一件有趣的事情可以测试。只是为了好玩... ConsoleHandle := RunApp('cmd.exe',PID);如果 ConsoleHandle 0 然后开始 S := 'Hello!?'; SendTextMessage(ConsoleHandle, WM_SETTEXT, 0, S); Writeln('我们在这里'); // 删除 FreeConsole 以在 RunApp 中进行测试 (un)comment Writeln here FreeConsole;睡眠(1000); // 如果 AttachConsole(PID) 尝试使用和不使用睡眠,那么 // 我们是否能够附加到我们进程的控制台? begin Writeln('我们在这里');免费控制台;结束;

以上是关于如何处理从我的 GUI 应用程序启动的控制台窗口?的主要内容,如果未能解决你的问题,请参考以下文章

Android:如何处理从右到左的滑动手势

如何处理从 Undertow 工作线程抛出的异常?

如果列数不同,如何处理从源 spark df 到 hive 表的插入

如何处理从 Gdiplus::DrawArc 返回的零星 OutOfMemory 错误代码?

如何处理从 ng-repeat 生成的列表单击并加载下一页

C#/JSON 如何处理从数据库查询返回的 NULL 值? [复制]