C#/Windows - Clipboard(Ctrl+C, Ctrl+V)가 동작하지 않는다면?
ctrl+c, ctrl+v가 어느 순간부터 동작하지 않았을 때, 다음과 같이 명령행에서 clip을 사용해 보면 "ERROR: Access is denied"인 경우를 볼 수 있습니다.
c:\temp> dir | clip
ERROR: Access is denied.
저 오류 메시지는 권한이 부족해서 발생하는 것이 아니고 clipboard 자원이 현재 다른 프로세스에 의해 잠겨 있기 때문일 수 있습니다. 실제로 한번 재현을 해볼까요? ^^
C#으로 다음과 같이 간단하게 프로그램을 만들고,
[DllImport("user32.dll", SetLastError = true)]
static extern bool CloseClipboard();
[DllImport("user32.dll", SetLastError = true)]
static extern bool OpenClipboard(IntPtr hWndNewOwner);
bool _opened = false;
void Button1_Click(object sender, EventArgs e)
{
_opened = OpenClipboard(this.Handle);
this.Text = _opened.ToString();
}
void Button2_Click(object sender, EventArgs e)
{
if (_opened == true)
{
CloseClipboard();
}
}
Button1만 눌러 Clipboard 자원을 Open 한 다음, 명령행에서 clip을 실행하면 "ERROR: Access is denied." 오류가 그대로 재현되는 것을 볼 수 있습니다.
그래서, ctrl+c/ctrl+v가 동작하지 않는 경우 Clipboard 자원을 Open만 하고 Close를 못 시킨 응용 프로그램을 종료하면 다시 정상적으로 동작하게 됩니다. 문제는? 도대체 어떤 프로그램이 소유하고 있는지 알 수 없으므로 현재 실행 중인 프로세스들을 임의로 모두 종료하는 식으로 대응해야 합니다.
명색이 프로그래머인데 ^^ 그러면 너무 없어 보이므로 간단하게 코드로,
How to check which application has the clipboard hold?
; https://superuser.com/questions/770476/how-to-check-which-application-has-the-clipboard-hold
작성해 명확하게 알아내는 것도 가능합니다.
[DllImport("user32.dll")]
static extern IntPtr GetOpenClipboardWindow();
[DllImport("user32.dll", SetLastError = true)]
static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
...[생략]...
{
IntPtr pClipboardOwner = GetOpenClipboardWindow();
if (pClipboardOwner == IntPtr.Zero)
{
return;
}
GetWindowThreadProcessId(pClipboardOwner, out uint dwProcessId);
Process owner = Process.GetProcessById((int)dwProcessId);
string name = owner.ProcessName;
string mainModule = owner.MainModule.FileName;
}
일단 GetWindowThreadProcessId Win32 API까지의 호출은 문제없는데, 이후의 Process 타입을 통해 정보를 알아오는 경우 호출 측과 대상 프로세스의 플랫폼 차이(x86/x64)가 발생하면 오류가 뜹니다. 이런 경우 해당 코드만 실행해 그 결과를 반환하는 간단한 EXE를 x86과 x64로 각각 빌드해 실행하는 것도 가능하겠지만, 그보다는 간단하게 WMI 쿼리를 사용하는 것도 나쁘지 않습니다.
{
// ...[생략]...
GetWindowThreadProcessId(pClipboardOwner, out uint dwProcessId);
textBox1.Text = $"{dwProcessId}, " + GetProcessInfo(dwProcessId);
}
string GetProcessInfo(uint processId)
{
string arg = $"process where \"ProcessID={processId}\" get ExecutablePath";
return GetOutput("wmic", arg);
}
private static string GetOutput(string executable, string arguments)
{
ProcessStartInfo psi = new ProcessStartInfo(); // wmic를 사용하지 않고 System.Management.dll을 이용해 결과를 반환하는 것도 가능
psi.FileName = executable;
psi.Arguments = arguments;
psi.RedirectStandardOutput = true;
psi.UseShellExecute = false;
psi.CreateNoWindow = true;
Process proc = Process.Start(psi);
string txt = proc.StandardOutput.ReadToEnd();
return txt;
}
참고로, OpenClipboard에 윈도우 핸들 값이 아닌 null을 주는 것도 가능합니다.
OpenClipboard(IntPtr.Zero);
위와 같은 방식으로 클립보드를 잠근 경우에는 다른 프로세스에서라도 CloseClipboard를 호출하면 풀리게 됩니다. 물론, 윈도우 핸들을 넘긴 경우에는 무조건 그 윈도우를 소유한 프로세스에서 CloseClipboard를 호출해야 합니다. (또는 종료하든가.)
첨부 파일은 이 글의 소스 코드와 실행 파일(WindowsFormsApp1.exe)을 담고 있습니다.
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]