前言:
最近正好寫一個程序,需要操作剪切板
功能很簡單,只需要從剪切板內讀取字符串,然后清空剪切板,然后再把字符串導入剪切板
我想當然的使用我最拿手的C#來完成這項工作,原因無他,因為.Net框架封裝了能實現這種功能的方法
然后就有了如下代碼


1 string Temp = ""; 2 while (true) 3 { 4 string Tex = Clipboard.GetText().ToString(); 5 if (!string.IsNullOrWhiteSpace(Tex) && Temp != Tex) 6 { 7 Clipboard.Clear(); 8 Clipboard.SetDataObject(Tex, false); 9 Temp = Tex; 10 } 11 Thread.Sleep(1); 12 }
這段代碼,也是網頁上廣泛流傳的,使用.Net框架操作系統剪切板的方法,當然這個方法在某些情況下很管用
不過在我這確發生了點問題,主要的問題有兩點
首先,我對剪切板的操作需求有實時性,也就是,操作人員復制的一瞬間就應該截取到剪切板的數據,處理完后再放入剪切板
結果
Clipboard.SetDataObject(Tex, false);
沒想到上面這條設置剪切板的指令竟然會卡焦點窗口的線程,比如說,我在A軟件執行了一次復制操作,如果使用了上述代碼,那么A軟件強制線程堵塞大概幾百毫秒的樣子,反正很影響體驗,我推測是因為該命令會鎖定內存導致的
那怎么辦,本著死馬當活馬醫的態度,我專門為該指令啟用了一個線程
Task.Factory.StartNew(()=> {Clipboard.Clear();Clipboard.SetDataObject(Text, false);});
使用了線程以后,因為操作滯后(線程啟動會延遲一會兒,并不實時)了,所以上述問題似乎解決了,但是沒想到出現了新的問題
string Tex = Clipboard.GetText().ToString();
上述從剪切板獲得字符串的指令,在默寫情況下,會卡滯住,然后程序在一分鐘之后,因為超時而被系統吊銷
emmmmm,在經過幾番努力之后,我終于意識到,雖然.Net封裝了不少操作系統API的方法,使得一些IO操作變簡單不少,但是帶來的問題也是同樣大的,在遇到無法解決的問題的時候,會有點束手無策
于是不得已,我只能放棄使用過C#完成該項功能,想著幸好功能簡單,而且操作WinAPI其實最好的還是使用C++來寫,于是我用C++復現了上述功能


1 #include "stdafx.h" 2 #include <windows.h> 3 #include <iostream> 4 using namespace std; 5 #pragma comment(linker,"/subsystem:windows /entry:mainCRTStartup") 6 7 int main(int argc, _TCHAR* argv[]) 8 { 9 HANDLE THandle = GlobalAlloc(GMEM_FIXED, 1000);//分配內存 10 char* Temp = (char*)THandle;//鎖定內存,返回申請內存的首地址 11 while (true) 12 { 13 HWND hWnd = NULL; 14 OpenClipboard(hWnd);//打開剪切板 15 if (IsClipboardFormatAvailable(CF_TEXT)) 16 { 17 HANDLE h = GetClipboardData(CF_TEXT);//獲取剪切板數據 18 char* p = (char*)GlobalLock(h); 19 GlobalUnlock(h); 20 if (strcmp(Temp, p)) 21 { 22 EmptyClipboard();//清空剪切板 23 HANDLE hHandle = GlobalAlloc(GMEM_FIXED, 1000);//分配內存 24 char* pData = (char*)GlobalLock(hHandle);//鎖定內存,返回申請內存的首地址 25 strcpy(pData, p); 26 strcpy(Temp, p); 27 SetClipboardData(CF_TEXT, hHandle);//設置剪切板數據 28 GlobalUnlock(hHandle);//解除鎖定 29 } 30 } 31 CloseClipboard();//關閉剪切板 32 Sleep(500); 33 } 34 return 0; 35 }
不愧是C++,使用上述代碼后,完美實現我需要的功能,而且不管是主程序,還是我寫的這個程序,都不會出現卡滯或者不工作的情況了,真是可喜可賀。
那么本教程就到此為止。
?
?
以下是正文
想著,既然我能用C++調用WinAPI完美實現我需要的功能,而且C#也能調用非托管的代碼來執行WinAPI,那么我不是可以把上面C++寫的代碼移植到C#里面執行?說干就干
首先,C#調用WinAPI需要先申明
[DllImport("User32")]internal static extern bool OpenClipboard(IntPtr hWndNewOwner);[DllImport("User32")]internal static extern bool CloseClipboard();[DllImport("User32")]internal static extern bool EmptyClipboard();[DllImport("User32")]internal static extern bool IsClipboardFormatAvailable(int format);[DllImport("User32")]internal static extern IntPtr GetClipboardData(int uFormat);[DllImport("User32", CharSet = CharSet.Unicode)]internal static extern IntPtr SetClipboardData(int uFormat, IntPtr hMem);
操作剪切板需要調用的API大致就上面這些
有了API以后,我們還需要自己手動封裝方法
internal static void SetText(string text){if (!OpenClipboard(IntPtr.Zero))
{
SetText(text);
return;
}EmptyClipboard();SetClipboardData(13, Marshal.StringToHGlobalUni(text));CloseClipboard();}internal static string GetText(int format){string value = string.Empty;OpenClipboard(IntPtr.Zero);if (IsClipboardFormatAvailable(format)){IntPtr ptr = NativeMethods.GetClipboardData(format);if (ptr != IntPtr.Zero){value = Marshal.PtrToStringUni(ptr);}}CloseClipboard();return value;}
我們也就用到兩個方法,從剪切板獲得文本和設置文本到剪切板,哦關于SetClipboardData的第一個參數13是怎么來的問題,其實這個剪切板的格式參數,下面有一張表,就是自從這里來的


public static class ClipboardFormat {/// <summary>/// Text format. Each line ends with a carriage return/linefeed (CR-LF) combination. A null character signals/// the end of the data. Use this format for ANSI text./// </summary>public const int CF_TEXT = 1;/// <summary>/// A handle to a bitmap (<c>HBITMAP</c>)./// </summary>public const int CF_BITMAP = 2;/// <summary>/// Handle to a metafile picture format as defined by the <c>METAFILEPICT</c> structure. When passing a/// <c>CF_METAFILEPICT</c> handle by means of DDE, the application responsible for deleting <c>hMem</c> should/// also free the metafile referred to by the <c>CF_METAFILEPICT</c> handle./// </summary>public const int CF_METAFILEPICT = 3;/// <summary>/// Microsoft Symbolic Link (SYLK) format./// </summary>public const int CF_SYLK = 4;/// <summary>/// Software Arts' Data Interchange Format./// </summary>public const int CF_DIF = 5;/// <summary>/// Tagged-image file format./// </summary>public const int CF_TIFF = 6;/// <summary>/// Text format containing characters in the OEM character set. Each line ends with a carriage return/linefeed/// (CR-LF) combination. A null character signals the end of the data./// </summary>public const int CF_OEMTEXT = 7;/// <summary>/// A memory object containing a <c>BITMAPINFO</c> structure followed by the bitmap bits./// </summary>public const int CF_DIB = 8;/// <summary>/// Handle to a color palette. Whenever an application places data in the clipboard that depends on or assumes/// a color palette, it should place the palette on the clipboard as well. If the clipboard contains data in/// the <see cref="CF_PALETTE"/> (logical color palette) format, the application should use the/// <c>SelectPalette</c> and <c>RealizePalette</c> functions to realize (compare) any other data in the/// clipboard against that logical palette. When displaying clipboard data, the clipboard always uses as its/// current palette any object on the clipboard that is in the <c>CF_PALETTE</c> format./// </summary>public const int CF_PALETTE = 9;/// <summary>/// Data for the pen extensions to the Microsoft Windows for Pen Computing./// </summary>public const int CF_PENDATA = 10;/// <summary>/// Represents audio data more complex than can be represented in a CF_WAVE standard wave format./// </summary>public const int CF_RIFF = 11;/// <summary>/// Represents audio data in one of the standard wave formats, such as 11 kHz or 22 kHz PCM./// </summary>public const int CF_WAVE = 12;/// <summary>/// Unicode text format. Each line ends with a carriage return/linefeed (CR-LF) combination. A null character/// signals the end of the data./// </summary>public const int CF_UNICODETEXT = 13;/// <summary>/// A handle to an enhanced metafile (<c>HENHMETAFILE</c>)./// </summary>public const int CF_ENHMETAFILE = 14;/// <summary>/// A handle to type <c>HDROP</c> that identifies a list of files. An application can retrieve information/// about the files by passing the handle to the <c>DragQueryFile</c> function./// </summary>public const int CF_HDROP = 15;/// <summary>/// The data is a handle to the locale identifier associated with text in the clipboard. When you close the/// clipboard, if it contains <c>CF_TEXT</c> data but no <c>CF_LOCALE</c> data, the system automatically sets/// the <c>CF_LOCALE</c> format to the current input language. You can use the <c>CF_LOCALE</c> format to/// associate a different locale with the clipboard text./// An application that pastes text from the clipboard can retrieve this format to determine which character/// set was used to generate the text./// Note that the clipboard does not support plain text in multiple character sets. To achieve this, use a/// formatted text data type such as RTF instead. /// The system uses the code page associated with <c>CF_LOCALE</c> to implicitly convert from/// <see cref="CF_TEXT"/> to <see cref="CF_UNICODETEXT"/>. Therefore, the correct code page table is used for/// the conversion./// </summary>public const int CF_LOCALE = 16;/// <summary>/// A memory object containing a <c>BITMAPV5HEADER</c> structure followed by the bitmap color space/// information and the bitmap bits./// </summary>public const int CF_DIBV5 = 17;/// <summary>/// Owner-display format. The clipboard owner must display and update the clipboard viewer window, and receive/// the <see cref="ClipboardMessages.WM_ASKCBFORMATNAME"/>, <see cref="ClipboardMessages.WM_HSCROLLCLIPBOARD"/>,/// <see cref="ClipboardMessages.WM_PAINTCLIPBOARD"/>, <see cref="ClipboardMessages.WM_SIZECLIPBOARD"/>, and/// <see cref="ClipboardMessages.WM_VSCROLLCLIPBOARD"/> messages. The <c>hMem</c> parameter must be <c>null</c>./// </summary>public const int CF_OWNERDISPLAY = 0x0080;/// <summary>/// Text display format associated with a private format. The <c>hMem</c> parameter must be a handle to data/// that can be displayed in text format in lieu of the privately formatted data./// </summary>public const int CF_DSPTEXT = 0x0081;/// <summary>/// Bitmap display format associated with a private format. The <c>hMem</c> parameter must be a handle to/// data that can be displayed in bitmap format in lieu of the privately formatted data./// </summary>public const int CF_DSPBITMAP = 0x0082;/// <summary>/// Metafile-picture display format associated with a private format. The <c>hMem</c> parameter must be a/// handle to data that can be displayed in metafile-picture format in lieu of the privately formatted data./// </summary>public const int CF_DSPMETAFILEPICT = 0x0083;/// <summary>/// Enhanced metafile display format associated with a private format. The <c>hMem</c> parameter must be a/// handle to data that can be displayed in enhanced metafile format in lieu of the privately formatted data./// </summary>public const int CF_DSPENHMETAFILE = 0x008E;/// <summary>/// Start of a range of integer values for application-defined GDI object clipboard formats. The end of the/// range is <see cref="CF_GDIOBJLAST"/>. Handles associated with clipboard formats in this range are not/// automatically deleted using the <c>GlobalFree</c> function when the clipboard is emptied. Also, when using/// values in this range, the <c>hMem</c> parameter is not a handle to a GDI object, but is a handle allocated/// by the <c>GlobalAlloc</c> function with the <c>GMEM_MOVEABLE</c> flag./// </summary>public const int CF_GDIOBJFIRST = 0x0300;/// <summary>/// See <see cref="CF_GDIOBJFIRST"/>./// </summary>public const int CF_GDIOBJLAST = 0x03FF;/// <summary>/// Start of a range of integer values for private clipboard formats. The range ends with/// <see cref="CF_PRIVATELAST"/>. Handles associated with private clipboard formats are not freed/// automatically; the clipboard owner must free such handles, typically in response to the/// <see cref="ClipboardMessages.WM_DESTROYCLIPBOARD"/> message./// </summary>public const int CF_PRIVATEFIRST = 0x0200;/// <summary>/// See <see cref="CF_PRIVATEFIRST"/>./// </summary>public const int CF_PRIVATELAST = 0x02FF; }
在C++里面是不用指定數字的,只需要用CF_UNICODETEXT就行,不過.Net里面應該沒有對應的索引表,所以只能手動輸入(我這里是為了說明用才專門用數字,自己代碼那是索引的枚舉類)
上面兩個工作做完以后,就能實現功能了,功能代碼如下
var LastS = string.Empty;while (!CancelInfoClipboard.IsCancellationRequested){var Temp = 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的時候,發現了一個函數
bool AddClipboardFormatListener(HWND hwnd);
根據描述來講,是添加一個剪切板的監控,在剪切板有任何變動的時候,通知你所指定的句柄的窗口,我一想,這不就是我所需要的么,有了這么一個API以后,其實我上面所展示的,使用死循環輪詢剪切板的方法就變得很傻逼,而且也很容易出錯了,于是,基于這個新發現的API,我重新更改了全部的程序邏輯,反而比原先的實現更加簡單了。
首先我們需要一個新的窗口或者控件來接收Windows消息更新后所發來的消息,只要New 一個form就行
public Form2(){InitializeComponent();AddClipboardFormatListener(this.Handle);}
然后我們在初始化組件的命令后面,把使用添加剪切板監聽的API把當前窗口的句柄發給系統,這樣系統在接收到剪切板改變的命令后,會把消息發給當前窗口
然后我們需要復寫WndProc方法
protected override void WndProc(ref Message m){if (m.Msg == 0x031D && Onice){var Temp = ClipboardControl.GetText(ClipboardFormat.CF_UNICODETEXT);if (!string.IsNullOrEmpty(Temp)){ClipboardControl.SetText(Temp);Onice = false;}}else if (!Onice){Onice = true;}else{base.WndProc(ref m);}}
? ?private bool Onice = true;
首先WndProc如果是Form類下面一個專門用來接收系統發送過來的消息的方法
然后關于m.Msg == 0x031D的0x031D在WinAPI定義上的意義是WM_CLIPBOARDUPDATE ,也就是剪切板更新事件,這個通過查找MSDN能夠找到
下面沒有特別奇怪的函數,就是有一點需要注意,我們這里設置了剪切板數據,相當于進行了一次更新,所以會在這一瞬間再次產生剪切板更新事件,然后又會通知這個方法,然后就會形成死循環,我在這里用了一個布爾判斷來通過布爾狀態決定是否截取剪切板,不只有有沒有更好的辦法來實現
以上