프로그램에서 만든 표나 굵은 글씨를 사용자가 워드, 아웃룩, 블로그 편집기에 바로 붙여 넣게 하고 싶을 때가 있습니다. 그런데 Clipboard.SetText("<b>굵게</b>") 로 넣으면 붙여 넣은 쪽에 태그가 글자 그대로 찍힙니다. 서식으로 인식되게 하려면 HTML을 Windows가 정한 HTML 클립보드 형식(CF_HTML)에 맞춰 넣어야 합니다.

조회 결과를 표로 만들어 메일 본문에 붙여 넣게 하거나, 보고서 일부를 서식째 복사해 주는 "복사" 버튼을 만들 때 이 방식이 쓰입니다. 엑셀이나 브라우저에서 무언가를 복사할 때도 클립보드에는 일반 텍스트와 함께 이 HTML 형식이 들어가고, 붙여 넣는 프로그램이 그중 자기가 다룰 수 있는 형식을 골라 갑니다.

광고

코드

    Private Sub AddHTMLToClipboard(ByVal html As String)
        Const sContextStart As String = "<HTML><BODY><!--StartFragment -->"
        Const sContextEnd As String = "<!--EndFragment --></BODY></HTML>"
        Const m_sDescription As String = "Version:1.0" & Constants.vbCrLf & "StartHTML:aaaaaaaaaa" + Constants.vbCrLf & "EndHTML:bbbbbbbbbb" + Constants.vbCrLf & "StartFragment:cccccccccc" + Constants.vbCrLf & "EndFragment:dddddddddd" + Constants.vbCrLf
        Dim sHtmlFragment As String = html
        Dim sData As String = m_sDescription & sContextStart & sHtmlFragment & sContextEnd
        sData = sData.Replace("aaaaaaaaaa", m_sDescription.Length.ToString().PadLeft(10, "0"c))
        sData = sData.Replace("bbbbbbbbbb", sData.Length.ToString().PadLeft(10, "0"c))
        sData = sData.Replace("cccccccccc", (m_sDescription & sContextStart).Length.ToString().PadLeft(10, "0"c))
        sData = sData.Replace("dddddddddd", (m_sDescription & sContextStart & sHtmlFragment).Length.ToString().PadLeft(10, "0"c))
        Clipboard.SetDataObject(New DataObject(DataFormats.Html, sData), True)
    End Sub

호출은 AddHTMLToClipboard("<b>굵게</b> 그리고 <i>기울임</i>") 처럼 HTML 조각만 넘기면 됩니다. <HTML><BODY> 같은 바깥 틀은 함수가 붙여 줍니다.

CF_HTML 형식이 요구하는 것

HTML 클립보드 데이터는 앞에 짧은 머리글이 붙습니다. Version, 그리고 전체 HTML이 어디서 시작하고 끝나는지(StartHTML, EndHTML), 실제로 붙여 넣을 조각이 어디서 시작하고 끝나는지(StartFragment, EndFragment)를 데이터 처음부터의 위치로 적어야 합니다. 붙여 넣는 프로그램은 이 숫자를 보고 조각만 잘라 갑니다. 조각 위치는 <!--StartFragment -->, <!--EndFragment --> 주석으로도 표시합니다.

문제는 위치 숫자를 적으면 머리글 길이가 바뀌고, 머리글 길이가 바뀌면 위치가 또 바뀐다는 점입니다. 그래서 이 코드는 각 자리를 aaaaaaaaaa 처럼 열 글자짜리 자리표시로 먼저 만들어 두고, 나중에 숫자를 열 자리로 0을 채워(PadLeft(10, "0"c)) 바꿔 넣습니다. 자리표시와 숫자의 길이가 같아서 치환해도 전체 길이가 변하지 않고, 처음에 계산한 위치가 그대로 맞습니다.

마지막 SetDataObject 의 두 번째 인자 True 는 프로그램을 닫은 뒤에도 클립보드 내용이 남게 하는 옵션입니다.

한글이 들어가면 위치가 어긋날 수 있다

CF_HTML 규격에서 이 위치 숫자는 문자 수가 아니라 UTF-8로 인코딩한 바이트 기준입니다. 코드는 String.Length(문자 수)로 계산하므로 영문만 있는 HTML에서는 맞지만, 한글처럼 한 글자가 여러 바이트인 문자가 들어가면 끝 위치가 실제보다 앞으로 계산됩니다. 그러면 붙여 넣은 결과 끝부분이 잘리거나 아예 서식 붙여넣기가 안 되는 일이 생길 수 있습니다. 한글을 넣을 거라면 길이 계산을 바이트 수로 바꾼 아래 형태로 시험해 보세요.

    Private Sub AddHTMLToClipboardUtf8(ByVal html As String)
        Const sContextStart As String = "<HTML><BODY><!--StartFragment -->"
        Const sContextEnd As String = "<!--EndFragment --></BODY></HTML>"
        Const m_sDescription As String = "Version:1.0" & vbCrLf & "StartHTML:aaaaaaaaaa" & vbCrLf & "EndHTML:bbbbbbbbbb" & vbCrLf & "StartFragment:cccccccccc" & vbCrLf & "EndFragment:dddddddddd" & vbCrLf
        Dim enc As System.Text.Encoding = System.Text.Encoding.UTF8
        Dim sData As String = m_sDescription & sContextStart & html & sContextEnd
        sData = sData.Replace("aaaaaaaaaa", enc.GetByteCount(m_sDescription).ToString().PadLeft(10, "0"c))
        sData = sData.Replace("bbbbbbbbbb", enc.GetByteCount(sData).ToString().PadLeft(10, "0"c))
        sData = sData.Replace("cccccccccc", enc.GetByteCount(m_sDescription & sContextStart).ToString().PadLeft(10, "0"c))
        sData = sData.Replace("dddddddddd", enc.GetByteCount(m_sDescription & sContextStart & html).ToString().PadLeft(10, "0"c))
        Clipboard.SetDataObject(New DataObject(DataFormats.Html, sData), True)
    End Sub

메모장에도 무언가 붙게 하려면

HTML만 넣으면 메모장처럼 서식을 모르는 프로그램에는 아무것도 붙지 않습니다. DataObject 를 하나 만들어 SetData(DataFormats.Html, ...) 와 SetData(DataFormats.Text, 일반텍스트) 를 둘 다 넣어 두면, 붙여 넣는 쪽이 알아서 맞는 형식을 골라 가서 어디에 붙여도 결과가 나옵니다. 위 함수의 마지막 줄을 이렇게 바꾸면 됩니다.

Dim obj As New DataObject()
obj.SetData(DataFormats.Html, sData)
obj.SetData(DataFormats.Text, plainText)
Clipboard.SetDataObject(obj, True)

plainText 는 같은 내용을 태그 없이 만든 문자열입니다. 표라면 칸 사이를 탭, 줄 사이를 줄바꿈으로 이어 두면 엑셀에 붙였을 때도 칸이 나뉘어 들어갑니다.

붙여 넣을 때 자주 걸리는 것들

표의 일부만 조각으로 넘기면 서식이 깨집니다. <tr> 이나 <td> 만 넘기지 말고 <table> 부터 닫는 태그까지 통째로 넘겨야 받는 프로그램이 표로 인식합니다. 이미지 태그의 src 가 상대 경로면 붙여 넣는 쪽에서 파일을 찾을 수 없으니, 웹에 올라가 있는 절대 URL을 쓰세요.

클립보드는 STA 스레드에서만 다룰 수 있습니다. 폼의 버튼 이벤트에서 부르면 문제없지만, BackgroundWorker 나 별도 스레드 안에서 이 함수를 부르면 ThreadStateException 이 납니다. 작업은 백그라운드에서 하고 클립보드에 넣는 부분만 Me.Invoke 로 UI 스레드에서 실행하면 됩니다. 다른 프로그램이 클립보드를 잠깐 붙잡고 있으면 ExternalException 이 날 때도 있는데, Clipboard.SetDataObject(obj, True, 5, 100) 처럼 재시도 횟수와 간격(밀리초)을 받는 오버로드를 쓰면 몇 번 다시 시도해 줍니다.