这个标题不太严谨,却也没想到更适合的表述,将就一下吧。
捣鼓桌面程序的人,或者对windows消息机制有所了解的,大概都知道其实windows桌面编程有一个贯穿了绝大多数消息机制的概念,就是容器。
比如一个按钮可以放在panel里,可以放在group里,也可以直接放在窗体上,这时候呢,这些panel、group和窗体就是这个按钮的容器。简而言之就是这个控件的“父容器”。
那么进程或者说sdi窗体的容器是谁呢?是桌面。桌面是一个较特殊的容器。
最近遇到个挺难受的问题,putty的新版自然是支持新的openssh交换协议,但是新版本全都没法记忆密码(当然带参数运行是可以的),而我用的0.57那个修改版太老了,每有新服务器就需要对openssh-server进行设置,次数多了也有点烦,于是最近试了一下最新的0.77。
并不意外的,新版仍然无法记忆密码。然后我又尝试了cnputty,当然,一样不行。
而且那个老版本有个我很喜欢的细节,就是当我复制了多行命令的时候,粘贴进去是可以直接运行的。而新版粘贴后显示为文本,需要手工敲一下回车。烦!
于是求助于各种管理工具,先后尝试了mremoteng、superputty,各有各的问题,相对而言superputty是比较符合我习惯的,然而这俩货都是.net整出来的,我个人比较反感在服务器上多装任何非必须程序。mremoteng的rdp协议做的很好,我一直在用,但是这货跟putty配合有些问题,那个0.57的版本无法最大化,始终是一个小窗口,这肯定不行。而superputty的rdp感觉太脑残了,基本上就是给mstsc传了个参数,至于用户名密码什么的都还要一次一次手工输入,肯定放弃了。
所以,我又试着自己干了。
我需要的功能都实现了,记录一些重点代码:
var hWin: HWND = 0; // 窗体枚举函数 function EnumWindowsProc(Wnd: HWND; ProcWndInfo: PProcessWindow): BOOL; stdcall; var WndProcessID: Cardinal; begin GetWindowThreadProcessId(Wnd, @WndProcessID); if WndProcessID = ProcWndInfo^.ProcessID then begin ProcWndInfo^.FoundWindow := Wnd; Result := False; // 已找到,故停止 EnumWindows end else Result := True; // 继续查找 end; // 由 ProcessID 查找窗体 Handle function GetProcessWindow(ProcessID: Cardinal): HWND; var ProcWndInfo: TProcessWindow; begin ProcWndInfo.ProcessID := ProcessID; ProcWndInfo.FoundWindow := 0; EnumWindows(@EnumWindowsProc, Integer(@ProcWndInfo)); // 查找窗体 Result := ProcWndInfo.FoundWindow; end; // 在 Panel 上内嵌运行程序 function RunAppInPanel(const AppFileName: string; ParentHandle: HWND; var WinHandle: HWND): Boolean; var si: STARTUPINFO; pi: TProcessInformation; begin Result := False; // 启动进程 FillChar(si, SizeOf(si), 0); si.cb := SizeOf(si); si.wShowWindow := SW_SHOW; if not CreateProcess(nil, PChar(AppFileName), nil, nil, true, CREATE_NEW_CONSOLE or NORMAL_PRIORITY_CLASS, nil, nil, si, pi) then Exit; // 等待进程启动 WaitForInputIdle(pi.hProcess, 10000); // 取得进程的 Handle WinHandle := GetProcessWindow(pi.dwProcessID); if WinHandle > 0 then begin // 设定父窗体 Windows.SetParent(WinHandle, ParentHandle); ShowWindow( WinHandle, SW_MAXIMIZE); // 设定窗体位置 // SetWindowPos(WinHandle, 0, 0, 0, 0, 0, SWP_NOSIZE or SWP_NOZORDER); // 去掉标题栏 SetWindowLong(WinHandle, GWL_STYLE, GetWindowLong(WinHandle, GWL_STYLE) and (not WS_CAPTION) and (not WS_BORDER) and (not WS_THICKFRAME)); UpdateWindow(WinHandle); Result := True; end; // 释放 Handle CloseHandle(pi.hProcess); CloseHandle(pi.hThread); end; procedure TForm1.FormResize(Sender: TObject); begin // 保持内嵌程序充满 pnlApp // if hWin <> 0 then MoveWindow(hWin, 0, 0, Panel1.ClientWidth, Panel1.ClientHeight, True); if hwin<>0 then SetForegroundWindow(hwin); end; procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction); begin // 退出时向内嵌程序发关闭消息 if hWin > 0 then PostMessage(hWin, WM_CLOSE, 0, 0); end; procedure TForm1.Button2Click(Sender: TObject); begin // Panel1.Align := alClient; // 启动内嵌程序 if not RunAppInPanel('D:\Tools\0 putty\0 putty 修改版.exe -ssh -l root -pw www.tingtao.org -C -load zdw -P 22 服务器IP', Panel1.Handle, hWin) then ShowMessage('App not found'); end;
其中重点就是CreateProcess和SetParent这两个步骤,或者说是RunAppInPanel这个函数。里面大部分都是对api的调用,所以用什么语言都差不多的。
SetParent,顾名思义,把目标进程的父容器设置为自己的panel,运行结果就是截图。
但是这些代码还并不完善,因为windows桌面程序有一个“当前进程”和“当前窗体”的概念,而把其他进程的容器设置为自己的panel以后呢,还并不能解决“焦点”的事,还需要后续步骤。
做到这一步,我忽然意识到后面要处理的细节太多了,恰好在搜索的过程中找到了个能同时对rdp和ssh都支持很好的软件,测试一天下来各种细节我都觉得满意了。明天分享。