UWSCでクリップボードを管理する

UWSCクリップボードを管理するモジュールを書いてみました。
C++でもVBでも同じですね。



気になった文言

UWSC公式掲示板で見かけたのです。

クリップボードのデータを、内容を問わずに一時待避させるような方法を見付けることができませんでした。
課題としては面白い内容なのですが実力不足で残念です。

ここは他の方の応援に期待したいです・・・

他の方、、、他の方、、、他の方、、、私か?
そうだ私に違いない!(自意識過剰!!!)


ということで。

スクリプト

Clipboard.uws

OPTION EXPLICIT

IFB GET_UWSC_NAME = "Clipboard.uws" THEN
	DIM loop = TRUE, i, num, p, j, fmt
	WHILE loop
		WHILE G_MOUSE_X
			SLEEP(0.1)
		WEND

		GETKEYSTATE(VK_CTRL)
		IF GETKEYSTATE(VK_CTRL) THEN Clipboard.Get()
		num = Clipboard.Count()
		p = SAFEARRAY(0, num * 4)
		FOR i = 1 TO num
			fmt = Clipboard.GetTypicalFormat(i)
			SELECT fmt
			CASE Clipboard.F_UNICODETEXT
				p[i*4-4] = Clipboard.Peek(fmt, i)
				j = POS("<#CR>", p[i*4-4])
				WHILE j = 1
					p[i*4-4] = COPY(p[i*4-4], 3)
					j = POS("<#CR>", p[i*4-4])
				WEND
				IF j THEN p[i*4-4] = COPY(p[i*4-4], 1, j - 1)
			DEFAULT
				p[i*4-4] = Clipboard.GetFormatName(fmt)
				IF LENGTH(p[i*4-4]) = 0 THEN p[i*4-4] = "fmt:" + fmt
			SELEND
			IF LENGTH(p[i*4-4]) = 0 THEN p[i*4-4] = "-"
			p[i*4-3] = "{&Set"
			p[i*4-2] = "&View"
			p[i*4-1] = "&Del}"
		NEXT
		p[num * 4] = "&End"
		j = POPUPMENU(p)
		i = INT(j / 4) + 1
		IFB j = -1 THEN
			// nop
		ELSEIF j = num * 4 THEN
			loop = FALSE
		ELSEIF j MOD 4 = 3 THEN
			Clipboard.Del(i)
		ELSEIF j MOD 4 = 2 THEN
			fmt = Clipboard.GetTypicalFormat(i)
			p = Clipboard.Peek(fmt, i, TRUE)
			SELECT fmt
			CASE Clipboard.F_UNICODETEXT
				PRINT p
			CASE Clipboard.F_HDROP
				FOR i = 0 TO LENGTH(p) - 1
					PRINT p[i]
				NEXT
			SELEND
			PRINT ""
		ELSE
			Clipboard.Set(i, FALSE)
		ENDIF
	WEND

	Clipboard.Dispose()
ENDIF


MODULE Clipboard
	DEF_DLL OpenClipboard(HWND): BOOL: user32
	DEF_DLL CloseClipboard(): BOOL: user32
	DEF_DLL EmptyClipboard(): BOOL: user32
	DEF_DLL CountClipboardFormats(): int: user32
	DEF_DLL EnumClipboardFormats(DWORD): DWORD: user32
	DEF_DLL GetClipboardFormatNameW(DWORD,var wstring,int): int: user32
	DEF_DLL GetClipboardData(DWORD): DWORD: user32
	DEF_DLL SetClipboardData(DWORD, DWORD): DWORD: user32

	DEF_DLL GlobalAlloc(DWORD, DWORD): DWORD: kernel32
	DEF_DLL GlobalLock(DWORD): DWORD: kernel32
	DEF_DLL GlobalUnlock(DWORD): BOOL: kernel32
	DEF_DLL GlobalSize(DWORD): DWORD: kernel32
	DEF_DLL GlobalFree(DWORD): DWORD: kernel32

	DEF_DLL GetProcessHeap(): DWORD: kernel32
	DEF_DLL HeapAlloc(DWORD, DWORD, DWORD): DWORD: kernel32
	DEF_DLL HeapFree(DWORD, DWORD, DWORD): BOOL: kernel32
	DEF_DLL HeapSize(DWORD, DWORD, DWORD): LONG: kernel32

	DEF_DLL RtlMoveMemory(DWORD, DWORD, DWORD): kernel32

	DEF_DLL DragQueryFileW(DWORD,DWORD,var wstring,DWORD): DWORD: shell32

	HASHTBL _datas
	DIM _hwnd, _heap
	CONST F_TEXT = 1
	CONST F_BITMAP = 2
	CONST F_METAFILEPICT = 3
	CONST F_SYLK = 4
	CONST F_DIF = 5
	CONST F_TIFF = 6
	CONST F_OEMTEXT = 7
	CONST F_DIB = 8
	CONST F_PALETTE = 9
	CONST F_PENDATA = 10
	CONST F_RIFF = 11
	CONST F_WAVE = 12
	CONST F_UNICODETEXT = 13
	CONST F_ENHMETAFILE = 14
	CONST F_HDROP = 15
	CONST F_LOCALE = 16
	CONST F_DIBV5 = 17

	PROCEDURE Clipboard
		_hwnd = IDTOHND(GETID(GET_THISUWSC_WIN))
		_heap = GetProcessHeap()
	FEND

	PROCEDURE Dispose()
		WHILE Del(); WEND
	FEND


	FUNCTION GetTypicalFormat(n=-1)
		RESULT = 0
		IF n = -1 THEN n = LENGTH(_datas)
		IF (n <= 0) OR (n > LENGTH(_datas)) THEN EXIT
		n = _datas[n - 1, HASH_KEY]
		DIM data = _datas[n], i, hMem, fmtName, size
		IF LENGTH(GetFormatName(data[0])) THEN RESULT = data[0]
		FOR i = 0 TO LENGTH(data) - 1 STEP 2
			SELECT data[i]
			CASE F_UNICODETEXT, F_DIBV5, F_HDROP
				RESULT = data[i]
				BREAK
			DEFAULT
				IFB RESULT = 0 THEN
					IF LENGTH(GetFormatName(data[i])) THEN RESULT = data[i]
				ENDIF
			SELEND
		NEXT
		IF RESULT = 0 THEN RESULT = data[0]
	FEND

	FUNCTION Trans(fmt)
		IF fmt = F_BITMAP OR fmt = F_DIB THEN fmt = F_DIBV5	// F_BITMAPではないけど、がんばれ!
		IF fmt = F_ENHMETAFILE THEN fmt = F_METAFILEPICT
		IF fmt = F_TEXT OR fmt = F_OEMTEXT THEN fmt = F_UNICODETEXT
		RESULT = fmt
	FEND

	FUNCTION Peek(fmt=F_HDROP, n=-1, p=FALSE)
		// 相互変換の読み替え
		fmt = Trans(fmt)
		IFB fmt = F_HDROP THEN
			RESULT = SPLIT(EMPTY)
		ELSEIF fmt = F_UNICODETEXT THEN
			RESULT = EMPTY
		ELSE
			RESULT = 0
		ENDIF
		IF n = -1 THEN n = LENGTH(_datas)
		IF (n <= 0) OR (n > LENGTH(_datas)) THEN EXIT
		n = _datas[n - 1, HASH_KEY]
		DIM data = _datas[n], i, hMem, size
		FOR i = 0 TO LENGTH(data) - 1 STEP 2
			IF p THEN PRINT "Peek " + data[i] + ":" + GetFormatName(data[i])
			IFB data[i] = fmt THEN
				hMem = data[i+1]
				IFB fmt = F_HDROP THEN
					DIM fileNum = 0, fileName, j
					IF hMem THEN fileNum = DragQueryFileW(hMem, -1, NULL, 0)
					IF fileNum THEN RESULT = SAFEARRAY(0, fileNum - 1)
					FOR j = 0 TO fileNum - 1
						size = DragQueryFileW(hMem, j, NULL, 0)
						IFB size THEN
							size = size + 1
							fileName = FORMAT(CHR(0), size)
							IF DragQueryFileW(hMem, j, fileName, size) THEN RESULT[j] = fileName
						ENDIF
					NEXT
				ELSEIF fmt = F_UNICODETEXT THEN
					size = HeapSize(_heap, 0, hMem)
					RESULT = FORMAT(CHR(0), size / 2)
					DEF_DLL RtlMoveMemory(var wstring, DWORD, DWORD): kernel32
					RtlMoveMemory(RESULT, hMem, size)
					DEF_DLL RtlMoveMemory(DWORD, DWORD, DWORD): kernel32
				ELSE
					RESULT = hMem
				ENDIF
			ENDIF
		NEXT
	FEND

	FUNCTION GetFormatName(fmt)
		DIM size = 64, fmtName = FORMAT(CHR(0), size)
		SELECT fmt
		CASE F_TEXT
			fmtName = "Text"
		CASE F_BITMAP
			fmtName = "Bitmap"
		CASE F_METAFILEPICT
			fmtName = "Meta File Pict"
		CASE F_SYLK
			fmtName = "SYLK"
		CASE F_DIF
			fmtName = "DIF"
		CASE F_TIFF
			fmtName = "Tiff"
		CASE F_OEMTEXT
			fmtName = "OEM Text"
		CASE F_DIB
			fmtName = "DIB"
		CASE F_PALETTE
			fmtName = "Palette"
		CASE F_PENDATA
			fmtName = "Pen Data"
		CASE F_RIFF
			fmtName = "Riff"
		CASE F_WAVE
			fmtName = "Wave"
		CASE F_UNICODETEXT
			fmtName = "Unicode Text"
		CASE F_ENHMETAFILE
			fmtName = "Enh-Meta File"
		CASE F_HDROP
			fmtName = "Files"
		CASE F_LOCALE
			fmtName = "Locale"
		CASE F_DIBV5
			fmtName = "Image"
		DEFAULT
			WHILE GetClipboardFormatNameW(fmt, fmtName, size - 1) >= size - 2
				size = size * 2
				fmtName = FORMAT(CHR(0), size)
			WEND
		SELEND
		RESULT = fmtName
	FEND

	FUNCTION Count()
		RESULT = LENGTH(_datas)
	FEND

	FUNCTION Check(fmt, hMem, size, n=-1)
		RESULT = FALSE
		EXIT	// 内容チェックを高速に行う方法を思いついたら真面目に実装する
		IF n = -1 THEN n = LENGTH(_datas)
		IF (n <= 0) OR (n > LENGTH(_datas)) THEN EXIT
		n = _datas[n - 1, HASH_KEY]
		DIM data = _datas[n], i, hMemTar, sizeTar
		FOR i = 0 TO LENGTH(data) - 1 STEP 2
			IFB fmt = data[i] THEN
				// サイズをチェックする
				// 同じなら内容をチェックする
				IFB size = HeapSize(_heap, 0, data[i+1]) THEN
				ENDIF
			ENDIF
		NEXT
	FEND

	FUNCTION Get()
		RESULT = OpenClipboard(_hwnd)
		IFB RESULT THEN
			DIM hClip, size, data = SAFEARRAY(0, 0), index = 0, bPrev = TRUE
			DIM hMem, fmt = EnumClipboardFormats(0)
			WHILE fmt <> 0
				hClip = GetClipBoardData(fmt)
				size = GlobalSize(hClip)
				hMem = GlobalLock(hClip)
				bPrev = bPrev AND Check(fmt, hMem, size)
				IFB fmt = Trans(fmt) THEN
					// CF_BITMAPはCF_DIBV5に期待して捨てる。CF_ENHMETAFILEは嫌な予感がするので捨てる
					// CF_DIB / CF_TEXT / CF_OEMTEXTも相互変換があるので捨てる(CF_DIBV5 と CF_UNICODETEXT)
					RESIZE(data, LENGTH(data) + 1)
					data[index] = fmt
					data[index+1] = HeapAlloc(_heap, 0, size)
					RtlMoveMemory(data[index+1], hMem, size)
					index = index + 2
				ENDIF
				GlobalUnlock(hClip)
				fmt = EnumClipboardFormats(fmt)
			WEND
			IFB LENGTH(data) > 1 AND !bPrev THEN
				// 一つ余分なので削る
				RESIZE(data, LENGTH(data) - 2)
				index = 1
				IF LENGTH(_datas) THEN index = VAL(_datas[LENGTH(_datas) - 1, HASH_KEY]) + 1
				_datas[index] = data
				RESULT = LENGTH(_datas)
			ELSEIF bPrev THEN
				RESULT = LENGTH(_datas)
			ELSE
				RESULT = 0
			ENDIF
			CloseClipboard()
		ELSE
			MSGBOX("OpenClipboard fail.")
		ENDIF
	FEND

	FUNCTION Set(n=-1, del=TRUE)
		IF n = -1 THEN n = LENGTH(_datas)
		RESULT = (n > 0) AND (n <= LENGTH(_datas))
		IFB RESULT THEN
			RESULT = OpenClipboard(_hwnd)
		ELSE
			MSGBOX("No Clipboard stack. " + n)
			EXIT
		ENDIF
		IFB RESULT THEN
			RESULT = EmptyClipboard()
		ELSE
			MSGBOX("OpenClipboard fail.")
			EXIT
		ENDIF
		IFB RESULT THEN
			DIM data = _datas[n - 1, HASH_VAL], size = LENGTH(data), index = 0, i = 0, j, k, m
			IF del THEN j = _datas[n, HASH_REMOVE]

			WHILE (index < size) AND (RESULT)
				k = HeapSize(_heap, 0, data[index+1])
				m = GlobalAlloc(2, k)
				j = GlobalLock(m)
				IFB j THEN
					RtlMoveMemory(j, data[index+1], k)
					GlobalUnlock(m)
					RESULT = (SetClipboardData(data[index], m) > 0)
				ELSE
					MSGBOX("GlobalLock fail.")
					RESULT = FALSE
				ENDIF
				IF del THEN HeapFree(_heap, 0, data[index+1])
				i = i + 1
				index = index + 2
			WEND

			CloseClipboard()
		ELSE
			CloseClipboard()
			MSGBOX("EmptyClipboard fail.")
		ENDIF
	FEND

	FUNCTION Del(n=-1)
		RESULT = FALSE
		IF n = -1 THEN n = LENGTH(_datas)
		IF (n <= 0) OR (n > LENGTH(_datas)) THEN EXIT
		DIM key = _datas[n - 1, HASH_KEY], data = _datas[key], i
		RESULT = _datas[key, HASH_REMOVE]
		FOR i = 1 TO LENGTH(data) - 1 STEP 2
			HeapFree(_heap, 0, data[i])
		NEXT
	FEND

ENDMODULE

クリップボードには歴史があるため、いろいろ古いAPIをルールに従って使う必要があるのです。


基本的な使い方は

CALL Clipboard  // して

// クリップボード操作前に待避
Clipboard.Get()


// クリップボードをいじり、終わったら
// クリップボードを戻す
Clipboard.Set()

という想定。


MODULE内部のHASHTBLにGet時の内容を積むので、Ctrl+CしてGetして、Ctrl+CしてGetして、最初のをSetして、、、なんてことも可能なはず。
Getのリターンは、0:失敗か、現在までの積んだ数。
オフィス付属のクリップボード管理と同じことができるのです!


なお、考えるのが面倒だったので、CF_BITMAPとCF_ENHMETAFILEは捨ててしまっています。
が、CF_BITMAPはCF_DIBまたはCF_DIBV5に変換されるため、絵も保持されます。
これらの自動変換を考えると、メモリーに無駄に待避してますが、、、ま、いいでしょ。


Peek関数は、クリップボードの中身を見るために作成しようとしました。
が、テキストの取得は標準関数でもできるので、ためしにファイルコピーを実装しました。
エクスプローラーでファイルを選択し、Ctrl+Cしてからこのスクリプトを実行すると、選択したファイルのフルパスがログ出力されると思います。

追記 2013/02/18

少し使い勝手を向上。
ちょっとしたバグの修正。
試験コードを、単体で利用できそうなものに変更。

  1. 実行する
  2. Ctrlキーを押しながら、マウスポインターを左端(x座標0)に移動すると、現在のクリップボードの内容を待避
  3. マウスポインターを左端に移動すると、現在保持している内容をPOPUPMENU表示

POPUPMENUから、クリップボードの差し替えや内容のログ出力・破棄が可能。
POPUPMENUの「End」を選択すると終了する。


DIBV5がクリップボードにある場合、POPUPMENUに表示しようかとも思ったけど、面倒になったのでやめた。
Win32APIを呼びまくれば可能。(ASMモジュールは不要)

追記 2013/02/21

POPUPMENUに画像表示する版を作成。
UWSCのPOPUPMENUにBitmapを表示する - じゅんじゅんのきまぐれ