Microsoft MVP성태의 닷넷 이야기
닷넷: 2333. C# - (Console 유형의 프로젝트에서) Clipboard 연동 [링크 복사], [링크+제목 복사],
조회: 1320
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일

C# - (Console 유형의 프로젝트에서) Clipboard 연동

ConsoleApp 유형에서 Clipboard를 쓰고 싶다면,

internal class Program
{
    static void Main(string[] args)
    {
        Clipboard.SetText("Hello, clipboard"); // How to copy data to clipboard in C#
    }
}

각각 Windows Forms와 WPF에서 제공하는 타입을 사용하면 됩니다.

Clipboard Class
; https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.clipboard

Clipboard Class
; https://learn.microsoft.com/en-us/dotnet/api/system.windows.clipboard

하지만, 위의 소스 코드 그대로 실행하면 이런 예외가 발생하는데요,

Unhandled exception. System.Threading.ThreadStateException: Current thread must be set to single thread apartment (STA) mode before OLE calls can be made. Ensure that your Main function has STAThreadAttribute marked on it.
   at System.Windows.Forms.Clipboard.SetDataObject(Object data, Boolean copy, Int32 retryTimes, Int32 retryDelay)
   at Program.Main(String[] args)

Clipboard가 OLE COM 호출을 하다 보니 STA 유형의 스레드에서만 사용할 수 있으므로 Main 함수에 해당 특성을 추가해야 합니다.

internal class Program
{
    [STAThread]
    static void Main(string[] args)
    {
        Clipboard.SetText("Hello, clipboard");
    }
}




그런데, 경우에 따라서는 저런 부가적인 라이브러리들이 붙는 것을 원치 않을 수도 있습니다. 그렇다면 Win32 API를 호출하면 되는데요, OpenClipboard에 윈도우 핸들을 전달하는 것으로 시작할 수 있습니다.

OpenClipboard function (winuser.h)
; https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-openclipboard

그렇다면 윈도우를 생성하기 위해 또다시 Windows Forms/WPF를 라이브러리를 추가하거나, 혹은 윈도우 생성을 위한 Win32 API를 추가로 복잡하게 구성해야 할까요? 마침 ^^ 지난 글에 설명한 GetConsoleWindow가 윈도우 핸들을 반환하므로 다음과 같이 소스 코드를 구성하는 것도 가능할 것입니다.

using System.Runtime.InteropServices;

namespace ConsoleApp2;

internal class Program
{
    [DllImport("kernel32.dll")]
    static extern IntPtr GetConsoleWindow();

    [DllImport("user32.dll", SetLastError = true)]
    static extern bool CloseClipboard();

    [DllImport("user32.dll", SetLastError = true)]
    static extern bool OpenClipboard(IntPtr hWndNewOwner);

    [DllImport("user32.dll", SetLastError = true)]
    static extern bool EmptyClipboard();

    [DllImport("user32.dll", SetLastError = true)]
    static extern IntPtr SetClipboardData(uint uFormat, IntPtr hMem);

    [STAThread]
    static void Main(string[] args)
    {
        IntPtr hwnd = GetConsoleWindow(); // 형식만 맞춘 것일 뿐, 의미 없는 윈도우 핸들에 불과합니다.
        if (hwnd == IntPtr.Zero)
        {
            Console.WriteLine("Failed to attach console");
            return;
        }

        if (OpenClipboard(hwnd) == false)
        {
            Console.WriteLine("Failed to open clipboard");
            return;
        }

        try
        {
            EmptyClipboard(); // empty 단계를 거치지 않으면 기존 데이터에 추가로 붙어버림
            SetClipboardData((int)ClipboardFormat.CF_UNICODETEXT, Marshal.StringToHGlobalUni($"{DateTime.Now} Hello, clipboard"));
        }
        finally
        {
            CloseClipboard();
        }
    }
}

// https://learn.microsoft.com/en-us/windows/win32/dataxchg/clipboard-formats
public enum ClipboardFormat
{
    CF_TEXT = 1,
    CF_BITMAP = 2,
    CF_METAFILEPICT = 3,
    CF_SYLK = 4,
    CF_DIF = 5,
    CF_TIFF = 6,
    CF_OEMTEXT = 7,
    CF_DIB = 8,
    CF_PALETTE = 9,
    CF_PENDATA = 10,
    CF_RIFF = 11,
    CF_WAVE = 12,
    CF_UNICODETEXT = 13,
}

그렇긴 한데, 굳이 OpenClipboard에 윈도우 핸들을 전달하지 않아도 잘 동작합니다.

OpenClipboard(IntPtr.Zero);

재미있는 건 공식 문서에는,

If an application calls OpenClipboard with hwnd set to NULL, EmptyClipboard sets the clipboard owner to NULL; this causes SetClipboardData to fail.


이렇게 설명하고 있는데, 그렇다면 분명히 SetClipboardData 호출이 실패해야 하지만... ^^; 잘됩니다.




그나저나, Clipboard Owner로 반드시 윈도우가 필요한 경우는 언제일까요? 이에 대해 검색해 보면,

How ownership of the Windows clipboard is tracked in Win32
; https://devblogs.microsoft.com/oldnewthing/20210526-00/?p=105252

"Delayed Rendering" 기능을 제공할 수 있다고 나옵니다. 이를 위해 우선 WM_RENDERFORMAT 메시지가 하는 역할을 설명하는데요,

WM_RENDERFORMAT message
; https://learn.microsoft.com/en-us/windows/win32/dataxchg/wm-renderformat

클립보드에 데이터를 제공할 측에서 SetClipboardData를 호출했을 때 첫 번째 인자인 uFormat만 전달하고, 두 번째 인자인 hMem 값은 null로 설정해 바로 데이터를 제공하지 않게 만들 수 있다고 합니다. 그렇게 되면, 클립보드 데이터를 가져오려는 시도가 발생했을 때, 그제야 (hMem에 해당했던) 데이터를 요구하는 WM_RENDERFORMAT 메시지가 전달된다고 합니다.

// Window Procedure

case WM_RENDERFORMAT:
    CLIPFORMAT cf = (CLIPFORMAT)wParam;
    hData = GenerateFormat(cf); // 메시지를 받은 시점에 클립보드 데이터를 생성
    SetClipboardData(cf, hData);
    return 0;

그다음, (WM_RENDERFORMAT과 연관되는) WM_DESTROY­CLIPBOARD가 있는데요,

WM_DESTROYCLIPBOARD message
; https://learn.microsoft.com/he-il/windows/win32/dataxchg/wm-destroyclipboard

이것은 "Delayed Rendering"으로 생성해서 반환했던 클립보드 데이터를 이제 삭제해도 된다는 신호입니다. 이어서, 글의 마지막에 이런 설명이 있는데요,

There is also special handling of the case where somebody passes NULL to Open­Clipboard, indicating that “nobody” is opening the clipboard.


따라서 null 윈도우 핸들 전달이 (실패와는 관계없이) 소유자를 지정하지 않는 특별한 사용 예의 하나라고 소개하고 합니다. (이외에도 Owner는 WM_RENDERALLFORMATS 메시지를 수신할 수 있습니다.)




근래에 나온 또 하나의 클립보드 이야기를 볼까요? ^^

Why doesn’t Clipboard History capture rapid changes to clipboard contents?
; https://devblogs.microsoft.com/oldnewthing/20250508-00/?p=111162

첫 번째 글에서는 클립보드에 넣을 몇 개의 데이터를 미리 설정하기 위해 OpenClipboard/EmptyClipboard/SetClipboardData/CloseClipboard 쌍을 연속으로 호출하고 있는데요,

using System.Runtime.InteropServices;

namespace ConsoleApp2;

internal class Program
{
    // ...[생략]...

    [STAThread]
    static void Main(string[] args)
    {
        IntPtr hwnd = GetConsoleWindow();
        if (hwnd == IntPtr.Zero)
        {
            Console.WriteLine("Failed to attach console");
            return;
        }

        for (int i = 0; i < 3; i++)
        {
            if (OpenClipboard(hwnd) == false)
            {
                Console.WriteLine("Failed to open clipboard");
                return;
            }

            string text = $"{DateTime.Now} Hello, clipboard: {i}";

            try
            {
                EmptyClipboard();
                SetClipboardData((int)ClipboardFormat.CF_UNICODETEXT, Marshal.StringToHGlobalUni(text));
            }
            finally
            {
                CloseClipboard();
            }
        }
    }
}

public enum ClipboardFormat
{
    // ...[생략]...
    CF_UNICODETEXT = 13,
}

의도한 바는 차례대로 모두 등록하기를 원했지만 실제로는 마지막 한 문장만 등록됩니다. 실제로 등록된 이력은 Clipboard Viewer를 이용해 확인할 수 있는데요, Window + V 단축키로 실행할 수 있습니다. (그리고, 그에 앞서 클립보드 이력은 설정이 필요합니다.)

이유는, 클립보드 이력이 내부적으로 비동기로 구현됐기 때문입니다. 작동 방식을 보면, 우선 클립보드 이력 서비스는 클립보드로부터 내용이 바뀌었다는 WM_CLIPBOARDUPDATE 메시지를 받기 위해 AddClipboardFormatListener 함수로 자신을 등록해 둔다고 합니다.

AddClipboardFormatListener
; https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-addclipboardformatlistener

즉, 클립보드 이력 서비스가 WM_CLIPBOARDUPDATE를 받아 처리하려고 하는 시점에는 이미 (위의 예제 프로그램 같은 경우) 마지막 3번째 SetClipboardText까지 호출을 완료했기 때문에 클립보드 이력 서비스는 마지막 메시지만 구할 수 있게 되는 것입니다.

그렇다면, 만약 위의 코드를 의도한 대로 동작하게 만들려면 어떻게 해야 할까요? 이에 대해서도 다음과 같은 글에서 설명하고 있습니다. ^^

How can I wait for Clipboard History to recognize a clipboard change before I change it again?
; https://devblogs.microsoft.com/oldnewthing/20250509-00/?p=111169

하지만 아쉽게도 WinRT API를 이용해야 하며 운영체제도 Windows 10 1809 이상에서만 동작하는데요,

Clipboard.HistoryChanged Event
; https://learn.microsoft.com/en-us/uwp/api/windows.applicationmodel.datatransfer.clipboard.historychanged?view=winrt-26100

그래도 C#에서는 쉽게 사용할 수 있습니다. 이를 위해 우선 csproj에 WinRT API를 사용하기 위한 설정을 추가하고,

<TargetFramework>net5.0-windows10.0.17763.0</TargetFramework>

이렇게 구현하면 됩니다. ^^

using Windows.ApplicationModel.DataTransfer;

internal class Program
{
    // ...[생략]...

    [STAThread]
    static void Main(string[] args)
    {
        // ...[생략]...

        if (Clipboard.IsHistoryEnabled() == false)
        {
            Console.WriteLine("Clipboard history is not enabled");
            Console.WriteLine("Please enable clipboard history in Windows settings.");
            Console.WriteLine("Settings > System > Clipboard, Turn on \"Clipboard history\" option.");
            return;
        }

        Clipboard.HistoryChanged += Clipboard_HistoryChanged;

        for (int i = 0; i < 3; i++)
        {
            // ...[생략]...

            _historyChanged.WaitOne();
            _historyChanged.Reset();
        }

        Clipboard.HistoryChanged -= Clipboard_HistoryChanged;
    }

    private static void Clipboard_HistoryChanged(object? sender, ClipboardHistoryChangedEventArgs e)
    {
        _historyChanged.Set();
    }
}

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




참고로, .NET Core/5+의 경우 Clipboard 관련 타입이 기본적인 ConsoleApp을 위한 어셈블리 Set에는 포함돼 있지 않으므로 그와 연관된 옵션을 csproj에 추가해야 합니다.

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net8.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <UseWindowsForms>true</UseWindowsForms>
        <!-- WPF인 경우라면 -->
        <!--
        <UseWPF>true</UseWPF>
        -->

    </PropertyGroup>

</Project>

또한, 이렇게 추가하고 빌드하면 이어서 NETSDK1136 컴파일 오류가 발생합니다.

error NETSDK1136: The target platform must be set to Windows (usually by including '-windows' in the TargetFramework property) when using Windows Forms or WPF, or referencing projects or packages that do so.


WPF와 Windows Forms가 윈도우 전용이다 보니, 아예 TargetFramework도 윈도우 전용으로 설정해야 하는 것입니다.

<TargetFramework>net8.0-windows</TargetFramework>




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 5/12/2025]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 181  182  [183]  184  185  186  187  188  189  190  191  192  193  194  195  ...
NoWriterDateCnt.TitleFile(s)
410정성태12/16/200621032오류 유형: 20. TFS SP1 설치 관련 오류 (1) - KB919156 패치
408정성태12/10/200623244Windows: 7. USB 드라이브 내용 암호화
407정성태12/10/200620801오류 유형: 19. Vista 에서의 VS.NET 2005 로 개발한 어셈블리에 대한 서명 확인 오류
406정성태12/10/200625543Windows: 6. IE 7 검색 공급자 - 영한 사전
403정성태12/6/200632646Windows: 5. Vista 와 웹 인증 등록 서비스의 문제 [5]
402정성태12/11/200623657Windows: 4. Vista 설치 후기 [1]
409정성태12/11/200627663    답변글 Windows: 4.1. Vista 설치 후기 - 두 번째 이야기 [3]
396정성태2/13/200730279오류 유형: 18. "Automatic Updates" 서비스 CPU 100% 점유 현상
393정성태11/8/200619852오류 유형: 17. Unable to start debugging - The binding handle is invalid.
371정성태10/23/200619045오류 유형: 16. STS Communication failed.
370정성태11/12/200622950.NET Framework: 75. Windows CardSpace 이야기 (이 글의 내용은 재작성되어질 예정입니다.)
375정성태10/25/200624849    답변글 .NET Framework: 75.1. 개인 발행 카드에 대한 Microsoft 예제 실습(이 글의 내용은 재작성되어질 예정입니다.)
376정성태10/27/200624530    답변글 .NET Framework: 75.2. "Windows CardSpace"와 "인증서 서비스"의 만남(이 글의 내용은 재작성되어질 예정입니다.)
377정성태10/26/200624276    답변글 .NET Framework: 75.3. Managed Card 발행에 대한 Microsoft 예제 실습 (1) - CardWriter (이 글의 내용은 재작성되어질 예정입니다.)
385정성태11/6/200626780    답변글 .NET Framework: 75.4. Managed Card 발행에 대한 Microsoft 예제 실습 (2) - STS 구현 (이 글의 내용은 재작성되어질 예정입니다.) [7]
387정성태11/2/200627658    답변글 .NET Framework: 75.5. Windows CardSpace와 SYSNET 사이트의 만남 (이 글의 내용은 재작성되어질 예정입니다.) [1]
397정성태11/11/200625127    답변글 .NET Framework: 75.6. CardWriter.csproj와 함께 알아보는 인증서 식별 방법(이 글의 내용은 재작성되어질 예정입니다.)
398정성태11/12/200623625    답변글 .NET Framework: 75.7. 카드에 암호 거는 방법(이 글의 내용은 재작성되어질 예정입니다.)
399정성태11/12/200625906    답변글 .NET Framework: 75.8. 인증서/스마트 카드에 기반한 Managed Card - STS 구현(이 글의 내용은 재작성되어질 예정입니다.) [5]
369정성태10/22/200621386오류 유형: 15. 자동 업데이트 실패
367정성태10/22/200637200Windows: 3. IIS 7.0 다중 바인딩 설정하는 방법 [1]
365정성태10/21/200620889Windows: 2. 서버(build 5600)에 IIS 7.0 서비스와 .NET 3.0 설치 방법
359정성태10/17/200616944오류 유형: 14. VS.NET 빌드 오류 - FxCopCmd.exe returned error code 65.
358정성태10/17/200622154오류 유형: 13. WSE 3.0 서비스 관련 WSE101 오류 / Destination Unreachable
357정성태12/1/200624428.NET Framework: 74. WCF 이야기 [4]
378정성태10/28/200629304    답변글 .NET Framework: 74.1. WCF와 WSE 3.0의 활용 [4]파일 다운로드1
... 181  182  [183]  184  185  186  187  188  189  190  191  192  193  194  195  ...