注:本文适用.net2.0+的winform程序

一个winform程序,我希望它不能多开(但是如何防多开不是本文要讲的),那么在用户启动第二个实例的时候,作为第二个实例来说,大概可以有这么几种做法:

  1. 弹个窗告知用户【程序已运行】之类,用户点击弹窗后,退出自身
  2. 什么都不做,默默退出自身
  3. 让已运行的第一个实例把它的窗体显示出来,完了退出自身

显然第3种做法更地道,实现该效果的核心问题其实是:如何显示指定进程的窗口?

首先想到的是调用ShowWindow、SetForegroundWindow等API,配合使用可以将被遮挡、最小化的窗口前排显示出来,这也是很多涉及到这种案例的网文介绍的方法,此法的局限在于,目标进程的主窗口必须存在,准确说是要有有效的主窗口句柄,表现在访问Process.MainWindowHandle能得到一个非IntPtr.Zero的值,即有效的句柄;或者用spy类工具能看到该进程下有至少一个窗口;或者按alt+tab能将它的窗口切换出来。

那如果进程没窗口怎么办?先说一下什么情况下进程会没窗口,很简单,让Form.Visible=false(或者Form.Hide(),等价的)就行,此时窗体就消失了,既不可见,也没有对应的任务栏按钮,alt+tab也切不出来。当程序中的所有Form都Hide后,访问该进程的MainWindowHandle会得到IntPtr.Zero,这就是无窗口进程。那什么样的程序会这么干,太多了好吧,各种音乐播放器,杀软什么的,都允许【关闭/最小化到系统托盘】,在你点叉或者最小化后,窗体就会隐藏,只留一个图标在托盘区。由于这种进程的MainWindowHandle拿不到有效句柄,所以上面那些API是用不了的,只能另想办法。

回到问题【如何显示指定进程的窗口】,如果你的程序不允许关闭到托盘区,始终存在窗口的话(最小化也是存在),那你愉快的用ShowWindow、SetForegroundWindow等API就好,不用继续。但如果你的程序要像播放器杀软那样允许用户隐藏窗口的话,那还得继续折腾,此时问题变成【如何让无窗口的进程显示窗口】,我的思路是这样:既然目标进程没窗口,我没办法纯粹用外部手段操作到它的窗体,但因为程序是我自己写的,可不可以来个里应外合,办了这事。比如向它发一条特定消息,它在收到该消息后,心领神会,把自己的窗口显示出来~到时候荣华富贵享之sorry入戏了。这个思路主要涉及两个问题,怎么发怎么收,至于收到后如何前排显示窗口之类,小case。

怎么发

SendMessage/PostMessage自然是指不上的,因为这俩货也是基于窗口的,其实我一度怀疑走消息这条路是否可行,这涉及到一个原理问题,就是如果消息一定是只能发送给窗口的话,那注定此路不通,只能考虑别的进程间通信方案。好在了解到PostThreadMessage这个API,解决了我的问题。该API是向指定线程发送消息(MSDN文档在此),这也说明在原理上,消息并非只可以发给窗口,还可以发给线程,至于还能不能发给别的什么东西就不知道了。先看一下发送语句:

void Main()
{
...
//向目标进程的主线程发送消息
PostThreadMessage(Process.GetProcessById(pid).Threads[].Id, 0x80F0, IntPtr.Zero, IntPtr.Zero);
...
} [return: MarshalAs(UnmanagedType.Bool)]
[DllImport("user32.dll", SetLastError = true)]
public static extern bool PostThreadMessage(int threadId, uint msg, IntPtr wParam, IntPtr lParam);

API的第1个参数是目标线程的ID。注意两点:①此ID是系统全局的线程ID,并非Thread.ManagedThreadId这种“假”ID;②目标线程必须存在消息循环。winform的主线程往往就是UI线程,天然存在消息循环,所以无需考虑这个问题。第2个参数是要发送的消息ID。我们的目的是发一条收发双方约定的消息,所以这个消息要够特别,不能跟系统消息撞衫,所以范围最好介于0x8001~0xBFFF之间,这是系统留给应用程序自用的消息段(WM_APP)。后面俩参数我没用,你想让消息更特别一点,或想携带其它信息的话也可以用上。方法返回true/false分别代表发送成功/失败。

另外,目标进程也许有多个线程,其中哪个才是能收消息的主线程我没有科学的判断方法,大胆臆测就是Process.Threads集合中的第1项,这个猜测至今工作良好,不管它。若您有科学判断法,请告知~谢谢。

怎么收

由于消息是走线程过来的,所以别想着在主窗口的WndProc中去收,再说消息过来的时候,主窗口存不存在都是个问题。要用应用程序级别的消息筛选器来收,筛选器是个实现System.Windows.Forms.IMessageFilter接口的类(MSDN),该接口只需实现一个方法:bool PreFilterMessage(ref Message m),方法的逻辑是,如果收到的消息m是你要处理并吃掉的,就返回true,其余消息则返回false放行。整个筛选器像这样:

class MsgFilter : IMessageFilter
{
public bool PreFilterMessage(ref Message m)
{
if (m.Msg == 0x80F0)
{
DoSomething(); //显示窗口或其它事
return true;
}
return false;
}
}

事实上我收到消息后并不是直接做显示窗口相关的事,而是引发一个事件,主窗体注册该事件,在事件处理方法中再写显示窗口相关的代码。这是设计上的考量,与本文主旨无关,不多说。

筛选器写好后,还得把它添加到一个地方它才能工作,什么时候添加就什么时候才开始发挥作用,所以最好尽早添加,例如在main的开头。像这样:

void Main()
{
Application.AddMessageFilter(new MsgFilter());
...
}

至此,收发的问题解决。这实质上是一个进程间通信问题,所以其实任何进程通信手段都可以应用在本文的案例,走消息只是其中一种手段。当然对于本文案例,若您有更好的办法,恳请告知,先行谢过。
-文毕-

【C#】给无窗口的进程发送消息的更多相关文章

  1. 【C#】无损转换Image为Icon 【C#】组件发布:MessageTip,轻快型消息提示窗 【C#】给无窗口的进程发送消息 【手记】WebBrowser响应页面中的blank开新窗口及window.close关闭本窗体 【手记】调用Process.EnterDebugMode引发异常:并非所有引用的特权或组都分配给呼叫方 【C#】DataRowState演变备忘

    [C#]无损转换Image为Icon 如题,市面上常见的方法是: var handle = bmp.GetHicon(); //得到图标句柄 return Icon.FromHandle(handle ...

  2. smbcontrol - 向smbd或nmbd进程发送消息

    总览smbcontrol [ -i ] smbcontrol [ 目标 ] [ 消息类型 ] [ 参数 ] 描述这个工具是是Samba组件的一部分. smbcontrol是个很小的程序,用它可以向系统 ...

  3. DelphiXE8FMX工程实现无边框托动(发送消息)

    1.引用单元 uses Winapi.Windows, FMX.Platform.Win, Winapi.Messages; 2.发送消息 //发送系统消息SendMessage(FmxHandleT ...

  4. 跨进程发送消息数据(发送WM_COPYDATA消息,够简单的)

    1 //1.发送窗体 2 procedure TForm2.Button1Click(Sender: TObject); 3 var 4 h: HWND; 5 Size: Integer; 6 Cop ...

  5. Android为TV端助力:EventBus跨进程发送消息

    单一app内的用法 如果你在单一app内进行多进程开发,那么只需要做以下三步: Step 1 在gradle文件中加入下面的依赖:   dependencies {   compile 'xiaofe ...

  6. C# 向指定的进程发送消息

    public static class ProcessExtensions { // Messages const int WM_KEYDOWN = 0x100; const int WM_KEYUP ...

  7. [转] C#中发送消息给指定的窗口,以及接收消息

    原文C#中发送消息给指定的窗口,以及接收消息 public class Note { //声明 API 函数 [DllImport("User32.dll", EntryPoint ...

  8. 在向server发送请求时发生传输级错误。 (provider: 共享内存提供程序, error: 0 - 管道的还有一端上无不论什么进程。

    作者:卿笃军 原文地址:http://blog.csdn.net/qingdujun/article/details/36425825 SQL Server 2008 + VS2010 用C#编写的代 ...

  9. 深入delphi编程理解之消息(六)无窗口单元消息的创建、接受及dispatch模式编程

    一.程序界面 二.程序代码 (一).主界面代码 //========================================================================== ...

随机推荐

  1. [C#] 进阶 - LINQ 标准查询操作概述

    LINQ 标准查询操作概述 序 “标准查询运算符”是组成语言集成查询 (LINQ) 模式的方法.大多数这些方法都在序列上运行,其中的序列是一个对象,其类型实现了IEnumerable<T> ...

  2. 问题记录:EntityFramework 一对一关系映射

    EntityFramework 一对一关系映射有很多种,比如主键作为关联,配置比较简单,示例代码: public class Teacher { public int Id { get; set; } ...

  3. 在Visual Studio Code中配置GO开发环境

    一.GO语言安装 详情查看:GO语言下载.安装.配置 二.GoLang插件介绍 对于Visual Studio Code开发工具,有一款优秀的GoLang插件,它的主页为:https://github ...

  4. MyBatis源码分析(二)语句处理器

    StatementHandler 语句处理器,主要负责语句的创建.参数的设置.语句的执行.不负责结果集的处理. Statement prepare(Connection connection, Int ...

  5. AFNetworking图片上传

    //上传图片 -(void)upLoadImage:(UIImage *)upImage { //创建管理 AFHTTPRequestOperationManager *manager = [AFHT ...

  6. vim的一些常用命令(一)

    先确认在Normal模式下,如不确认,请按几次Esc. :sy on/clear sy表示语法高亮,on是打开.clear是取消. :set go= set表示一般性设置,go是gui option的 ...

  7. NoSql1 在Linux(CentOS)上安装memcached及使用

    前言:       今天是初五,生活基本要从过年的节奏中回归到正常的生活了,所以想想也该想想与工作有关的事情了.我之前在工作中会经常使用memcached和redis,但是自己一直没有时间系统的好好看 ...

  8. 【Java并发编程实战】----- AQS(三):阻塞、唤醒:LockSupport

    在上篇博客([Java并发编程实战]----- AQS(二):获取锁.释放锁)中提到,当一个线程加入到CLH队列中时,如果不是头节点是需要判断该节点是否需要挂起:在释放锁后,需要唤醒该线程的继任节点 ...

  9. Linux网络编程-tcp缓存设置

    最近发现服务的逻辑完成时间很短,但是上游接收到的时间比较长,所以就怀疑是底层数据的序列化/反序列化.读写.传输有问题,然后怀疑是TCP的读写缓存是不是设置太小.现在就记录下TCP缓存的各配置项以及缓存 ...

  10. 提高(Android)开发效率的工具与网站

    Dev_Tools 写这篇不是为了"水",是因为有些工具确实不错,但是换个电脑要找到或者在某个时候你想用但是你只知道存在了书签里.然后就只能下载chrome然后登录账户同步书签了. ...