Goでクリップボードにテキスト/ファイルリストの読み書きをする（Windows）

Goにもクリップボード使えるライブラリありますが、ファイルリストの読み書きできるのなさそうなので作ってみました。Windows限定ですけど


```clip.go
package main

import (
	"fmt"
	"syscall"
	"unsafe"

	"github.com/AllenDang/w32"
)

// #include <string.h>
import "C"

type clipboardFormat uint

const (
	textFormat     clipboardFormat = w32.CF_UNICODETEXT
	fileListFormat clipboardFormat = w32.CF_HDROP
)

func isAvailable(format clipboardFormat) bool {
	return w32.IsClipboardFormatAvailable(uint(format))
}

func getText() (string, error) {
	if w32.OpenClipboard(0) {
		defer w32.CloseClipboard()
		hclip := w32.HGLOBAL(w32.GetClipboardData(w32.CF_UNICODETEXT))
		if hclip == 0 {
			return "", fmt.Errorf("GetClipboardData")
		}

		u16pstr := w32.GlobalLock(hclip)
		defer w32.GlobalUnlock(hclip)
		return w32.UTF16PtrToString((*uint16)(u16pstr)), nil
	}
	return "", fmt.Errorf("OpenClipboard")
}

func getFileList() ([]string, error) {
	if w32.OpenClipboard(0) {
		defer w32.CloseClipboard()
		hclip := w32.HDROP(w32.GetClipboardData(w32.CF_HDROP))
		if hclip == 0 {
			return nil, fmt.Errorf("GetClipboardData")
		}

		_, count := w32.DragQueryFile(hclip, 0xffffffff)
		files := make([]string, count)
		for i := uint(0); i < count; i++ {
			files[i], _ = w32.DragQueryFile(hclip, i)
		}
		return files, nil
	}
	return nil, fmt.Errorf("OpenClipboard")
}

func setText(text string) error {
	if w32.OpenClipboard(0) {
		defer w32.CloseClipboard()
		u16str, err := syscall.UTF16FromString(text)
		if err != nil {
			return fmt.Errorf("UTF16FromString")
		}

		hmem := w32.GlobalAlloc(w32.GHND, uint32(2*len(u16str)))
		outPstr := w32.GlobalLock(hmem)
		defer w32.GlobalUnlock(hmem)

		w32.Lstrcpy((*[1]uint16)(outPstr)[:], &u16str[0])
		w32.EmptyClipboard()
		w32.SetClipboardData(w32.CF_UNICODETEXT, w32.HANDLE(hmem))
	}
	return fmt.Errorf("OpenClipboard")
}

func buildU16files(files []string) ([]uint16, error) {
	var u16files []uint16
	for _, s := range files {
		u16str, err := syscall.UTF16FromString(s)
		if err != nil {
			return nil, fmt.Errorf("UTF16FromString")
		}
		u16files = append(u16files, u16str...)
	}
	u16files = append(u16files, 0)
	return u16files, nil
}
func setFileList(files []string) error {
	if w32.OpenClipboard(0) {
		defer w32.CloseClipboard()
		u16files, err := buildU16files(files)
		if err != nil {
			return err
		}

		type POINT struct {
			x int32
			y int32
		}
		type DROPFILES struct {
			pFiles w32.DWORD
			pt     POINT
			fNC    w32.BOOL
			fWide  w32.BOOL
		}

		dfiles := DROPFILES{fWide: w32.BoolToBOOL(true)}
		dfiles.pFiles = w32.DWORD(unsafe.Sizeof(dfiles))

		hmem := w32.GlobalAlloc(w32.GHND, uint32(dfiles.pFiles)+uint32(2*len(u16files)))
		outFiles := w32.GlobalLock(hmem)
		defer w32.GlobalUnlock(hmem)

		C.memcpy(outFiles, unsafe.Pointer(&dfiles), C.size_t(dfiles.pFiles))
		C.memcpy(unsafe.Pointer(uintptr(outFiles)+uintptr(dfiles.pFiles)),
			unsafe.Pointer(&u16files[0]), C.size_t(2*len(u16files)))

		w32.EmptyClipboard()
		w32.SetClipboardData(w32.CF_HDROP, w32.HANDLE(hmem))
	}
	return fmt.Errorf("OpenClipboard")
}

func main() {
	if isAvailable(textFormat) {
		text, err := getText()
		if err != nil {
			fmt.Println("error:", err)
			return
		}
		fmt.Println("clipdata[text]:", text)
		setText("text:" + text)

	} else if isAvailable(fileListFormat) {
		files, err := getFileList()
		if err != nil {
			fmt.Println("error:", err)
			return
		}
		fmt.Println("clipdata[fileList]:", files)
		if len(files) > 0 {
			setFileList(files[1:])
		}
	}
}
```

# テキストの読み込み

`CF_UNICODETEXT`を指定して`GetClipboardData`でハンドルとってきて、`GlobalLock`でデータとってくればOK

# ファイルリストの読み込み

`DragQueryFile`に`0xffffffff`を指定するとファイルリストの件数が取れます。
その後、件数分`DragQueryFile`を読んでファイルパスを取得しています。

# テキストの書き込み

`GlobalAlloc`で書き込み先のメモリを確保して、`GlobalLock`で書き込み先のポインタを取得。その後、書き込み先に文字列コピーしています。

`EmptyClipboard`をしないと直前にクリップボードを使っていたアプリ内での貼り付けがうまくいかないことがあります。

`(*[1]uint16)(outPstr)[:]`は、unsafe.Pointerからスライスへの変換です。`(*[0]uint16)`にするとpanicになります。（なぜ？）

# ファイルリストの書き込み

ファイルリストの書き込みでは、書き込み先にDROPFILES構造体と"ファイル\0ファイル\0 ... ファイル\0\0"を書き込む必要があります。

`C.memcpy`の部分は`w32.MoveMemory`でもできそうです。

# main部分

この部分はサンプルコードで、テキストの場合はクリップボードの先頭に"text:"を追加、ファイルリストの場合はファイルを1つ減らすようにしています。