最近正好写一个程序,需要操作剪切板
功能很简单,只需要从剪切板内读取字符串,然后清空剪切板,然后再把字符串导入剪切板
我想当然的使用我最拿手的C#来完成这项工作,原因无他,因为.Net框架封装了能实现这种功能的方法
然后就有了如下代码
1stringTemp="";2while(true)3{4stringTex=Clipboard.GetText().ToString();5if(!string.IsNullOrWhiteSpace(Tex)&&Temp!=Tex)6{7Clipboard.Clear();8Clipboard.SetDataObject(Tex,false);9Temp=Tex;10}11Thread.Sleep(1);12}ViewCode这段代码,也是网页上广泛流传的,使用.Net框架操作系统剪切板的方法,当然这个方法在某些情况下很管用
不过在我这确发生了点问题,主要的问题有两点
首先,我对剪切板的操作需求有实时性,也就是,操作人员复制的一瞬间就应该截取到剪切板的数据,处理完后再放入剪切板
结果
Clipboard.SetDataObject(Tex,false);没想到上面这条设置剪切板的指令竟然会卡焦点窗口的线程,比如说,我在A软件执行了一次复制操作,如果使用了上述代码,那么A软件强制线程堵塞大概几百毫秒的样子,反正很影响体验,我推测是因为该命令会锁定内存导致的
那怎么办,本着死马当活马医的态度,我专门为该指令启用了一个线程
Task.Factory.StartNew(()=>{Clipboard.Clear();Clipboard.SetDataObject(Text,false);});使用了线程以后,因为操作滞后(线程启动会延迟一会儿,并不实时)了,所以上述问题似乎解决了,但是没想到出现了新的问题
stringTex=Clipboard.GetText().ToString();上述从剪切板获得字符串的指令,在默写情况下,会卡滞住,然后程序在一分钟之后,因为超时而被系统吊销
emmmmm,在经过几番努力之后,我终于意识到,虽然.Net封装了不少操作系统API的方法,使得一些IO操作变简单不少,但是带来的问题也是同样大的,在遇到无法解决的问题的时候,会有点束手无策
于是不得已,我只能放弃使用过C#完成该项功能,想着幸好功能简单,而且操作WinAPI其实最好的还是使用C++来写,于是我用C++复现了上述功能
那么本教程就到此为止。
想着,既然我能用C++调用WinAPI完美实现我需要的功能,而且C#也能调用非托管的代码来执行WinAPI,那么我不是可以把上面C++写的代码移植到C#里面执行?说干就干
首先,C#调用WinAPI需要先申明
[DllImport("User32")]internalstaticexternboolOpenClipboard(IntPtrhWndNewOwner);[DllImport("User32")]internalstaticexternboolCloseClipboard();[DllImport("User32")]internalstaticexternboolEmptyClipboard();[DllImport("User32")]internalstaticexternboolIsClipboardFormatAvailable(intformat);[DllImport("User32")]internalstaticexternIntPtrGetClipboardData(intuFormat);[DllImport("User32",CharSet=CharSet.Unicode)]internalstaticexternIntPtrSetClipboardData(intuFormat,IntPtrhMem);操作剪切板需要调用的API大致就上面这些
有了API以后,我们还需要自己手动封装方法
internalstaticvoidSetText(stringtext){if(!OpenClipboard(IntPtr.Zero)){SetText(text);return;}EmptyClipboard();SetClipboardData(13,Marshal.StringToHGlobalUni(text));CloseClipboard();}internalstaticstringGetText(intformat){stringvalue=string.Empty;OpenClipboard(IntPtr.Zero);if(IsClipboardFormatAvailable(format)){IntPtrptr=NativeMethods.GetClipboardData(format);if(ptr!=IntPtr.Zero){value=Marshal.PtrToStringUni(ptr);}}CloseClipboard();returnvalue;}我们也就用到两个方法,从剪切板获得文本和设置文本到剪切板,哦关于SetClipboardData的第一个参数13是怎么来的问题,其实这个剪切板的格式参数,下面有一张表,就是自从这里来的
上面两个工作做完以后,就能实现功能了,功能代码如下
varLastS=string.Empty;while(!CancelInfoClipboard.IsCancellationRequested){varTemp=ClipboardControl.GetText(ClipboardFormat.CF_UNICODETEXT);if(!string.IsNullOrEmpty(Temp)&&Temp!=LastS){ClipboardControl.SetText(Temp);LastS=Temp;}Thread.Sleep(50);}是不是和最开始展示的调用.Net框架的方法一模一样(笑),不过使用底层API实现的功能,就没有那么多乱七八糟的Bug了,自己也很清楚到底实现了啥功能,同时也收获了不少新知识(主要是非托管代码调用的时候的注意事项什么的,还有,向非托管代码传递数据的时候,最好多用Marshal类里面的方法,不然可能会出错,毕竟这个类就是专门为非托管代码而设立的)
在研究MSDN上面关于剪切板的API的时候,发现了一个函数
boolAddClipboardFormatListener(HWNDhwnd);根据描述来讲,是添加一个剪切板的监控,在剪切板有任何变动的时候,通知你所指定的句柄的窗口,我一想,这不就是我所需要的么,有了这么一个API以后,其实我上面所展示的,使用死循环轮询剪切板的方法就变得很傻逼,而且也很容易出错了,于是,基于这个新发现的API,我重新更改了全部的程序逻辑,反而比原先的实现更加简单了。
首先我们需要一个新的窗口或者控件来接收Windows消息更新后所发来的消息,只要New一个form就行
publicForm2(){InitializeComponent();AddClipboardFormatListener(this.Handle);}然后我们在初始化组件的命令后面,把使用添加剪切板监听的API把当前窗口的句柄发给系统,这样系统在接收到剪切板改变的命令后,会把消息发给当前窗口
然后我们需要复写WndProc方法
protectedoverridevoidWndProc(refMessagem){if(m.Msg==0x031D&&Onice){varTemp=ClipboardControl.GetText(ClipboardFormat.CF_UNICODETEXT);if(!string.IsNullOrEmpty(Temp)){ClipboardControl.SetText(Temp);Onice=false;}}elseif(!Onice){Onice=true;}else{base.WndProc(refm);}}privateboolOnice=true;首先WndProc如果是Form类下面一个专门用来接收系统发送过来的消息的方法
然后关于m.Msg==0x031D的0x031D在WinAPI定义上的意义是WM_CLIPBOARDUPDATE,也就是剪切板更新事件,这个通过查找MSDN能够找到
下面没有特别奇怪的函数,就是有一点需要注意,我们这里设置了剪切板数据,相当于进行了一次更新,所以会在这一瞬间再次产生剪切板更新事件,然后又会通知这个方法,然后就会形成死循环,我在这里用了一个布尔判断来通过布尔状态决定是否截取剪切板,不只有有没有更好的办法来实现