VBAでクリップボードにHTMLテキストをコピーする

# HTMLテキストのクリップボード形式
HTMLテキストのクリップボード形式（形式名：HTML Format）はテキストデータですが、ヘッダを付ける点と文字エンコードがUTF-8である点で通常のテキストと異なるようです。また、テキストは部分的なHTMLでなく、フラグメントとコンテキストに分けて記録します。

+ フラグメント：主要な（選択した）HTML要素
+ コンテキスト：フラグメントの周囲の関係するタグも含めたHTML要素（`<HTML> ～ </HTML>`）

下記はEdgeで[Qiita](https://qiita.com/, "Qiita Home")を開いてアドレスバーからコピーした際のクリップボードですが、クリップボードのバージョン番号やコンテキストとフラグメントの開始位置と終了位置を含めたヘッダがあり、また`a`タグを`<!--StartFragment-->`と`<!--EndFragment-->`で囲む構造をしています。
```
Version:0.9
StartHTML:0000000105
EndHTML:0000000215
StartFragment:0000000141
EndFragment:0000000179
<html>
<body>
<!--StartFragment--><a href="https://qiita.com/">Qiita</a><!--EndFragment-->
</body>
</html>
```

VBAはバイナリ操作もUTF-8エンコードも不得意なので Windows API と ADODB.Stream オブジェクトを利用してコードを記述しました。コードの複雑化を避けるためオフセットの計算はマジックナンバーとしています。

# VBAコード
### クリップボードへHTML形式テキストのコピーと読み取り
```vb:VBA
' クリップボード操作関数
Private Declare PtrSafe Function CloseClipboard Lib "user32" () As Long
Private Declare PtrSafe Function EmptyClipboard Lib "user32" () As Long
Private Declare PtrSafe Function EnumClipboardFormats Lib "user32" (ByVal wFormat As Long) As Long
Private Declare PtrSafe Function GetClipboardData Lib "user32" (ByVal wFormat As Long) As LongPtr
Private Declare PtrSafe Function GetClipboardFormatName Lib "user32" Alias "GetClipboardFormatNameA" (ByVal wFormat As Long, ByVal lpString As String, ByVal nMaxCount As Long) As Long
Private Declare PtrSafe Function OpenClipboard Lib "user32" (ByVal hWnd As LongPtr) As Long
Private Declare PtrSafe Function SetClipboardData Lib "user32" (ByVal wFormat As Long, ByVal hMem As LongPtr) As Long

' メモリ管理関数
Private Declare PtrSafe Function GlobalAlloc Lib "kernel32" (ByVal wFlags As Long, ByVal dwBytes As LongLong) As LongPtr
Private Declare PtrSafe Function GlobalFree Lib "kernel32" (ByVal hMem As LongPtr) As Long
Private Declare PtrSafe Function GlobalLock Lib "kernel32" (ByVal hMem As LongPtr) As LongPtr
Private Declare PtrSafe Function GlobalUnlock Lib "kernel32" (ByVal hMem As LongPtr) As Long
Private Declare PtrSafe Sub MoveMemory Lib "kernel32" Alias "RtlMoveMemory" (ByVal Destination As LongPtr, ByVal Source As LongPtr, ByVal Length As LongLong)
Private Declare PtrSafe Function RegisterClipboardFormat Lib "user32" Alias "RegisterClipboardFormatA" (ByVal lpString As String) As Long

' 文字列操作関数
Private Declare PtrSafe Function lstrlen Lib "kernel32" Alias "lstrlenA" (ByVal lpString As LongPtr) As Long


' クリップボードにaタグをコピーして取得します
Public Sub SetClipboardTest()
    SetClipboardHTML "https://qiita.com", "<a href=""https://qiita.com/"">Qiita</a>"
    Debug.Print GetClipbordHTML
End Sub


' クリップボードにテキストとHTMLテキストをコピーします、成否を返します
Private Function SetClipboardHTML(sText As String, sHtml As String) As Boolean
    Const GHND As Long = &H42
    Const CF_UNICODETEXT As Long = 13
    
    ' 新しいクリップボード形式を登録します
    Dim CF_HTML As Long: CF_HTML = RegisterClipboardFormat("HTML Format")
    If CF_HTML = 0 Then Exit Function
    
    ' クリップボードを開きます
    If OpenClipboard(0) = 0 Then Exit Function ' クリップボードを開くのに失敗
    On Error GoTo Finally ' エラーが発生したら Close するようにします
    EmptyClipboard
    
    ' テキストをクリップボードにコピーします
    Dim hText As LongPtr: hText = GlobalAlloc(GHND, LenB(sText) + 2)
    If hText = 0 Then GoTo Finally ' 関数に失敗
    Dim pText As LongPtr: pText = GlobalLock(hText)
    If pText = 0 Then GlobalFree hText: GoTo Finally ' 関数に失敗
    MoveMemory pText, StrPtr(sText), LenB(sText)
    GlobalUnlock hText
    If SetClipboardData(CF_UNICODETEXT, hText) = 0 Then
        ' コピー失敗
        GlobalFree hText
        GoTo Finally
    End If
    
    ' HTMLテキストをUTF-8エンコードの文字列でクリップボードにコピーします
    With CreateObject("ADODB.Stream")
        .Type = 2 ' adTypeText
        .Charset = "UTF-8"
        .Open
        .WriteText "Version:0.9" & vbCrLf
        .WriteText "StartHTML:000089" & vbCrLf
        .WriteText "EndHTML:000000" & vbCrLf
        .WriteText "StartFragment:000125" & vbCrLf
        .WriteText "EndFragment:000000" & vbCrLf
        .WriteText "<html>" & vbCrLf
        .WriteText "<body>" & vbCrLf
        .WriteText "<!--StartFragment-->"
        .WriteText sHtml
        .WriteText "<!--EndFragment-->" & vbCrLf
        .WriteText "</body>" & vbCrLf
        .WriteText "</html>"
        .WriteText vbNullChar ' NULL末端
        Dim byteLength As Long: byteLength = CLng(.Position) - 3 ' BOMを除く
        .Position = 84 ' EndFragment:
        .WriteText Format(byteLength - 37, "000000")
        .Position = 42 ' EndHTML:
        .WriteText Format(byteLength - 1, "000000")
        .Position = 0
        .Type = 1 ' adTypeBinary
        .Position = 3 ' BOMを除く
        Dim bytes() As Byte: bytes = .Read
    End With
    Dim hHtml As LongPtr: hHtml = GlobalAlloc(GHND, byteLength)
    If hHtml = 0 Then GoTo Finally ' 関数に失敗
    Dim pHtml As LongPtr: pHtml = GlobalLock(hHtml)
    If pHtml = 0 Then GlobalFree hHtml: GoTo Finally ' 関数に失敗
    MoveMemory pHtml, VarPtr(bytes(0)), byteLength
    GlobalUnlock hHtml
    If SetClipboardData(CF_HTML, hHtml) = 0 Then
        ' コピー失敗
        GlobalFree hHtml
        GoTo Finally
    End If
    SetClipboardHTML = True
    
Finally:
    CloseClipboard
End Function


' クリップボードからHTML形式のテキストを取得します
Private Function GetClipbordHTML() As String
    If OpenClipboard(0) = 0 Then Exit Function ' クリップボードを開くのに失敗
    On Error GoTo Finally ' エラーが発生したら Close するようにします
Continue:
    ' クリップボード形式を列挙します
    Dim cf As Long: cf = EnumClipboardFormats(cf)
    
    ' 戻り値が 0 の場合は列挙終了
    If cf = 0 Then GoTo Finally
    
    ' 標準のクリップボード形式の場合は次へ
    If cf < &HC000& Then GoTo Continue
    
    ' クリップボード形式名を取得します
    Dim cfName As String * 100
    If GetClipboardFormatName(cf, cfName, LenB(cfName)) = 0 Then GoTo Finally
    
    ' クリップボード形式名が HTML Format でない場合は次へ
    If Left(cfName, InStr(cfName, vbNullChar) - 1) <> "HTML Format" Then GoTo Continue
    
    ' ハンドルを取得します
    Dim hMem As LongPtr: hMem = GetClipboardData(cf)
    If hMem = 0 Then GoTo Finally ' 関数が失敗
    
    ' メモリアドレスを取得します
    Dim pMem As LongPtr: pMem = GlobalLock(hMem)
    If pMem = 0 Then GoTo Finally ' 関数が失敗
    
    ' 文字列のバイト数を取得します
    Dim sLength As Long: sLength = lstrlen(pMem)
    If sLength <= 0 Then GoTo Finally ' 関数が失敗
    
    ' バッファにUTF-8エンコードテキストをコピーします
    Dim sBuffer() As Byte: ReDim sBuffer(1 To sLength)
    MoveMemory VarPtr(sBuffer(1)), pMem, sLength
    
    ' UTF-8からUnicodeに変換します
    With CreateObject("ADODB.Stream")
        .Type = 1 ' adTypeBinary
        .Open
        .Write sBuffer
        .Position = 0
        .Type = 2 ' adTypeText
        .Charset = "UTF-8"
        GetClipbordHTML = .ReadText
    End With
    
Finally:
    If pMem Then GlobalUnlock hMem
    CloseClipboard
End Function
```

### クリップボード内のデータ形式を列挙
おまけ
```vb:VBA
' クリップボード操作関数
Private Declare PtrSafe Function CloseClipboard Lib "user32" () As Long
Private Declare PtrSafe Function EnumClipboardFormats Lib "user32" (ByVal wFormat As Long) As Long
Private Declare PtrSafe Function GetClipboardFormatName Lib "user32" Alias "GetClipboardFormatNameA" (ByVal wFormat As Long, ByVal lpString As String, ByVal nMaxCount As Long) As Long
Private Declare PtrSafe Function OpenClipboard Lib "user32" (ByVal hWnd As LongPtr) As Long


' イミディエイトウィンドウにクリップボード形式を列挙する
Public Sub PrintClipboardFormats()
    OpenClipboard 0
    Do
        Dim cf As Long: cf = EnumClipboardFormats(cf)
        If cf = 0 Then Exit Do
        If cf < &HC000& Then
            Dim cfName As String
            Select Case cf
                Case 1: cfName = "CF_TEXT"
                Case 2: cfName = "CF_BITMAP"
                Case 3: cfName = "CF_METAFILEPICT"
                Case 4: cfName = "CF_SYLK"
                Case 5: cfName = "CF_DIF"
                Case 6: cfName = "CF_TIFF"
                Case 7: cfName = "CF_OEMTEXT"
                Case 8: cfName = "CF_DIB"
                Case 9: cfName = "CF_PALETTE"
                Case 9: cfName = "CF_PALETTE"
                Case 11: cfName = "CF_RIFF"
                Case 12: cfName = "CF_WAVE"
                Case 13: cfName = "CF_UNICODETEXT"
                Case 14: cfName = "CF_ENHMETAFILE"
                Case 15: cfName = "CF_HDROP"
                Case 16: cfName = "CF_LOCALE"
                Case 17: cfName = "CF_DIBV5"
                Case &H80&: cfName = "CF_OWNERDISPLAY"
                Case &H81&: cfName = "CF_DSPTEXT"
                Case &H82&: cfName = "CF_DSPBITMAP"
                Case &H83&: cfName = "CF_DSPMETAFILEPICT"
                Case &H8E&: cfName = "CF_DSPENHMETAFILE"
                Case &H82&: cfName = "CF_GDIOBJFIRST"
                Case &H200&: cfName = "CF_PRIVATEFIRST"
                Case &H2FF&: cfName = "CF_PRIVATELAST"
                Case &H300&: cfName = "CF_DSPBITMAP"
                Case &H300&: cfName = "CF_DSPBITMAP"
                Case &H300&: cfName = "CF_DSPBITMAP"
                Case &H3FF&: cfName = "CF_GDIOBJLAST"
                Case Else: cfName = "--Unknown--"
            End Select
        Else
            Dim sBuffer As String * 255: If GetClipboardFormatName(cf, sBuffer, LenB(sBuffer)) = 0 Then Exit Do
            cfName = Left(sBuffer, InStr(sBuffer, vbNullChar) - 1)
        End If
        Debug.Print Right("000" & WorksheetFunction.Dec2Hex(cf), 4), cfName
    Loop
    CloseClipboard
End Sub
```


# 参考
https://learn.microsoft.com/ja-jp/windows/win32/dataxchg/using-the-clipboard

https://learn.microsoft.com/ja-jp/windows/win32/dataxchg/html-clipboard-format

https://learn.microsoft.com/ja-jp/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767917(v=vs.85)