有时候给程序添加新功能时,原程序结构可能要面临很大的改变,这时候更倾向于使用类似插件的形式去处理新功能。
具体为新建一个窗体界面项目去处理新功能,并生成exe文件。在主项目需要的时候执行该exe文件,并进行通信,使用SendMessage进行进程间通信。
目录
具体通信如下:
1.主程序启动带参数的EXE
(1)在合适的地方启动exe文件:
Process pro = new Process();
string arguments = string.Format("{0} {1}", "传入的参数1", "传入的参数2"); //可不传,也可传入多个参数,每个参数之间用空格隔开
ProcessStartInfo proStartInfo = new ProcessStartInfo(@"E:\yuanyuxin\测试代码\嵌入4gexe\bin\Debug\嵌入4gexe.exe", arguments);//exe的路径
//proStartInfo.WindowStyle = ProcessWindowStyle.Hidden;
pro.StartInfo = proStartInfo;
pro.Start();//运行exe
上面传参数要注意一个问题,每个参数里面不能含有空格,如上面的例子你把参数1为“my mes”,最后被解析的时候就相当于传入3个参数了。要传带空格的字符串的话最简单就是把参数用自己定义的分隔符组成一个传入,再在 步骤1(2)把解析的数据重新组合起来分解成你要的参数
主程序关闭的时候,exe也对应关闭。
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
if(!pro.HasExited)
pro?.Kill();//结束进程
}
或者如果不是全局变量,可以通过名称查找
Process[] pro = Process.GetProcesses();//获取已开启的所有进程
//遍历所有查找到的进程
for (int i = 0; i < pro.Length; i++)
{
//判断此进程是否是要查找的进程
if (pro[i].ProcessName == "exe名称")
{
if(!pro.HasExited)
pro?.Kill();//结束进程
}
}
(2)在exe项目文件下找到Program.cs文件:
,修改Main方法,如果不需要传参数则不用修改:
static void Main(string[] args)
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
if (args.Length < 2)
{
args = new string[2] { "参数1", "参数2" };//这里为了独自运行exe文件时默认值
}
Application.Run(new Form_show(args[0], args[1]));//args[0]就是传入的参数1,args[1]就是传入的参数2
}
(3)在exe项目文件下找到窗体文件
对传入的参数去处理
2.主程序向exe发消息:
(1)主程序引用所需的dll文件
在主程序需要发送消息的地方添加如下代码:
[DllImport("User32.dll", EntryPoint = "SendMessage")]
private static extern int SendMessage(int hWnd, int Msg, int wParam, ref COPYDATASTRUCT lParam);
[DllImport("User32.dll", EntryPoint = "FindWindow")]
private static extern int FindWindow(string lpClassName, string lpWindowName);
const int WM_COPYDATA = 0x004A;//当一个应用程序传递数据给另一个应用程序时发送此消息
public struct COPYDATASTRUCT
{
public IntPtr dwData;//用户定义数据
public int cbData;//数据大小
//public string devId;
[MarshalAs(UnmanagedType.LPStr)]
public string lpData; //指向数据的指针
}
(2)主程序发送信息给exe
需要和上面步骤(1)在同一个类中:
data="要发送的消息";
byte[] sarr = System.Text.Encoding.Default.GetBytes(data);
int len = sarr.Length;
COPYDATASTRUCT cds;
cds.dwData = (IntPtr)Convert.ToInt16(1);//可以是任意值
cds.cbData = len + 1;//指定lpData内存区域的字节数
cds.lpData = data;//发送给目标窗口所在进程的数据
//cds.devId = devid;
SendMessage((int)pro.MainWindowHandle, WM_COPYDATA, 0, ref cds);//发送消息
一般来说,会把要传递的消息放在lpData 中,如果有两个数据要发送,可以用逗号或者其他字符连接,然后接收的地方去处理,还有另一种方式是用dwData保存存储地址,在接收的地方去解析,这样的方式要注意可能不是同一个指向地址,可能取不到,需要进行处理,保存在统一的地址,一般不复杂的数据格式不建议这样做。
另外需要说明的是,如果exe被隐藏,上面的句柄是无法发送成功的,需要用FindWindow函数获取句柄
var hanl = FindWindow(null, "嵌入4gexe");//第一个填类名,第二个填窗口的标题
SendMessage(hanl, WM_COPYDATA, 0, ref cds);
(3)exe接收到主程序的消息
protected override void DefWndProc(ref Message m)
{
switch (m.Msg)
{
case WM_COPYDATA:
COPYDATASTRUCT cds = new COPYDATASTRUCT();
Type t = cds.GetType();
cds = (COPYDATASTRUCT)m.GetLParam(t);//得到主程序发送的数据
//以下为逻辑处理
string strResult = cds.lpData;
string strType = cds.dwData.ToString();
switch (strType)
{
case "1":
MessageBox.Show("接收到数据:"+strResult );
break;
default:
break;
}
break;
default:
base.DefWndProc(ref m);
break;
}
}
3.exe向主程序返回消息:
(1)exe项目添加发送代码
同样的,exe添加2(1)的代码,同样的给主程序发送消息
data="要发送的消息";
byte[] sarr = System.Text.Encoding.Default.GetBytes(data);
int len = sarr.Length;
COPYDATASTRUCT cds;
cds.dwData = (IntPtr)Convert.ToInt16(1);//可以是任意值
cds.cbData = len + 1;//指定lpData内存区域的字节数
cds.lpData = data;//发送给目标窗口所在进程的数据
var pro = Process.GetProcesses();//查找所有的进程
foreach (Process p in pro)
{
if (p.ProcessName == "WinForm嵌入EXE程序"|| p.Handle== HWnd)//进程名为主程序名,或者主程序传入消息的时候就保存主程序的句柄
{
richTextBox1.AppendText("发送消息的名称和句柄:" + p.ProcessName.ToString()+","+HWnd + "\n");
SendMessage((int)p.MainWindowHandle, WM_COPYDATA, 0, ref cds);
}
}
ps:最好不用上面的方式找句柄(当主程序打开下拉框或者菜单的时候,会发现通信不通),用下面的方式
var hanl = FindWindow(null, _appName);
var sendResult = SendMessage(hanl, WM_COPYDATA, 0, ref cds);
(2)主程序添加接收消息函数
同2(3)的步骤所示,主程序也添加DefWndProc函数来接收消息。
这里注意的是因为主程序逻辑比较复杂,有时候给exe发送消息的时候是用的子界面或者其他的地方,要注意此时exe接收到的句柄一般也是主程序主界面的句柄,那返回的消息,也是传递到主界面DefWndProc中,在子界面是无法接收到DefWndProc消息的。一般的处理方法是在主程序接收到消息后,再传递给子界面。
消息大全汇总:https://blog.csdn.net/Yyuanyuxin/article/details/106767561
4.有关通信不通的调试参考
1.确保发送使用的是FindWindow(null, _appName)
找发送句柄。
_appName的地方填写的窗体的名称,即如下所示:
2.在接收方函数里面添加返回值
m.Result = (IntPtr)1; //任意设置值,不要用0,因为发送失败值为0
3.在发送端查看是否发送成功
5.传递信息为数组
上面的方式只是传递字符串类型,如果要传递的是byte数组,单纯的上述方式无法实现,需要在非托管内存中开一块用于数据传输,代码如下:
原来的结构体修改为:
[StructLayout(LayoutKind.Sequential)]
public struct COPYDATASTRUCT
{
public IntPtr dwData;
public int cbData;
public IntPtr lpData;
}
发送函数:
public void SendMessageToProcess(byte[] message, int cmdCode)
{
int len = message.Length;
COPYDATASTRUCT cds;
cds.dwData = (IntPtr)message[2];//命令码
cds.cbData = len + 1;//指定lpData内存区域的字节数
IntPtr iPtr = Marshal.AllocHGlobal(message.Length);
Marshal.Copy(message, 0, iPtr, message.Length);
cds.lpData = iPtr;
var hanl = FindWindow(null, ExeName);
SendMessage(hanl, WM_COPYDATA, 0, ref cds);
//释放非托管内存
Marshal.FreeCoTaskMem(iPtr);
}
接收代码(注意接收软件的结构体也要保持一致):
protected override void DefWndProc(ref Message m)
{
switch (m.Msg)
{
case WM_COPYDATA:
COPYDATASTRUCT cds = new COPYDATASTRUCT();
Type mytype = cds.GetType();
cds = (COPYDATASTRUCT)m.GetLParam(mytype);
uint flag = (uint)(cds.dwData);
byte[] bt = new byte[cds.cbData];
Marshal.Copy(cds.lpData, bt, 0, bt.Length);
//bt就是接收的数组
break;
default:
base.DefWndProc(ref m);
break;
}
}
c++和c#之间的进程通信可以参考这篇文章,写的很详细:点击此处
注意:该消息只能由SendMessage()来发送,而不能使用PostMessage()。因为系统必须管理用以传递数据的缓冲区的生命期,如果使用了PostMessage(),数据缓冲区会在接收方(线程)有机会处理该数据之前,就被系统清除和回收。此外如果lpData指向一个带有指针或某一拥有虚函数的对象时,也要小心处理。
如果传入的句柄不是一个有效的窗口或当接收方进程意外终止时,SendMessage()会立即返回,因此发送方在这种情况下不会陷入一个无穷的等待状态中。
弹窗后界面卡死的解决办法
在DefWndProc事件中,谨慎加入弹窗(如Message.Show()),弹窗会导致消息堵塞,新的消息无法进入,导致界面卡死。