본문 바로가기
[Unity]

[Unity] Windows 파일 드래그 앤 드롭 구현하기(Drag and Drop)

by 김기승 2023. 9. 10.

Unity에서 사용자가 파일 경로를 직접 입력해서, 파일 입출력을 해야하는 경우도 종종 있다.

만약, 접근하고자 하는 파일의 개수가 적다면 모르겠지만, 많다면 불편함을 겪게 될 것이다.

 

이번 글에서는 간단히 파일을 드래그 앤 드롭하는 것만으로도 파일 경로를 가져올 수 있는 방법에 대해 알아보겠다.


Script

이번 기능은 직접 구현한 것은 아니고, 

Bunny83님의 UnityWindowsFileDrag-Drop 레포지토리를 참고했다.

https://github.com/Bunny83/UnityWindowsFileDrag-Drop

 

GitHub - Bunny83/UnityWindowsFileDrag-Drop: Adds file drag and drop support for Unity standalong builds on windows.

Adds file drag and drop support for Unity standalong builds on windows. - GitHub - Bunny83/UnityWindowsFileDrag-Drop: Adds file drag and drop support for Unity standalong builds on windows.

github.com

 

① B83.Win32.cs

여기서 핵심 스크립트인 B83.Win32.cs만 있으면 간단히 구현할 수 있다.

코드 정리를 일부 거쳐 스크립트를 작성하였다.

※ 내용이 꽤나 길다.

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;

namespace B83.Win32
{
    public enum HookType : int
    {
        WH_JOURNALRECORD = 0,
        WH_JOURNALPLAYBACK = 1,
        WH_KEYBOARD = 2,
        WH_GETMESSAGE = 3,
        WH_CALLWNDPROC = 4,
        WH_CBT = 5,
        WH_SYSMSGFILTER = 6,
        WH_MOUSE = 7,
        WH_HARDWARE = 8,
        WH_DEBUG = 9,
        WH_SHELL = 10,
        WH_FOREGROUNDIDLE = 11,
        WH_CALLWNDPROCRET = 12,
        WH_KEYBOARD_LL = 13,
        WH_MOUSE_LL = 14
    }

    public enum WindowsMessage : uint
    {
        NULL = 0x0000,
        CREATE = 0x0001,
        DESTROY = 0x0002,
        MOVE = 0x0003,
        SIZE = 0x0005,
        ACTIVATE = 0x0006,
        SETFOCUS = 0x0007,
        KILLFOCUS = 0x0008,
        ENABLE = 0x000A,
        SETREDRAW = 0x000B,
        SETTEXT = 0x000C,
        GETTEXT = 0x000D,
        GETTEXTLENGTH = 0x000E,
        PAINT = 0x000F,
        CLOSE = 0x0010,
        QUERYENDSESSION = 0x0011,
        QUERYOPEN = 0x0013,
        ENDSESSION = 0x0016,
        QUIT = 0x0012,
        ERASEBKGND = 0x0014,
        SYSCOLORCHANGE = 0x0015,
        SHOWWINDOW = 0x0018,
        WININICHANGE = 0x001A,
        SETTINGCHANGE = WININICHANGE,
        DEVMODECHANGE = 0x001B,
        ACTIVATEAPP = 0x001C,
        FONTCHANGE = 0x001D,
        TIMECHANGE = 0x001E,
        CANCELMODE = 0x001F,
        SETCURSOR = 0x0020,
        MOUSEACTIVATE = 0x0021,
        CHILDACTIVATE = 0x0022,
        QUEUESYNC = 0x0023,
        GETMINMAXINFO = 0x0024,
        PAINTICON = 0x0026,
        ICONERASEBKGND = 0x0027,
        NEXTDLGCTL = 0x0028,
        SPOOLERSTATUS = 0x002A,
        DRAWITEM = 0x002B,
        MEASUREITEM = 0x002C,
        DELETEITEM = 0x002D,
        VKEYTOITEM = 0x002E,
        CHARTOITEM = 0x002F,
        SETFONT = 0x0030,
        GETFONT = 0x0031,
        SETHOTKEY = 0x0032,
        GETHOTKEY = 0x0033,
        QUERYDRAGICON = 0x0037,
        COMPAREITEM = 0x0039,
        GETOBJECT = 0x003D,
        COMPACTING = 0x0041,
        [Obsolete]
        COMMNOTIFY = 0x0044,
        WINDOWPOSCHANGING = 0x0046,
        WINDOWPOSCHANGED = 0x0047,
        [Obsolete]
        POWER = 0x0048,
        COPYDATA = 0x004A,
        CANCELJOURNAL = 0x004B,
        NOTIFY = 0x004E,
        INPUTLANGCHANGEREQUEST = 0x0050,
        INPUTLANGCHANGE = 0x0051,
        TCARD = 0x0052,
        HELP = 0x0053,
        USERCHANGED = 0x0054,
        NOTIFYFORMAT = 0x0055,
        CONTEXTMENU = 0x007B,
        STYLECHANGING = 0x007C,
        STYLECHANGED = 0x007D,
        DISPLAYCHANGE = 0x007E,
        GETICON = 0x007F,
        SETICON = 0x0080,
        NCCREATE = 0x0081,
        NCDESTROY = 0x0082,
        NCCALCSIZE = 0x0083,
        NCHITTEST = 0x0084,
        NCPAINT = 0x0085,
        NCACTIVATE = 0x0086,
        GETDLGCODE = 0x0087,
        SYNCPAINT = 0x0088,
        NCMOUSEMOVE = 0x00A0,
        NCLBUTTONDOWN = 0x00A1,
        NCLBUTTONUP = 0x00A2,
        NCLBUTTONDBLCLK = 0x00A3,
        NCRBUTTONDOWN = 0x00A4,
        NCRBUTTONUP = 0x00A5,
        NCRBUTTONDBLCLK = 0x00A6,
        NCMBUTTONDOWN = 0x00A7,
        NCMBUTTONUP = 0x00A8,
        NCMBUTTONDBLCLK = 0x00A9,
        NCXBUTTONDOWN = 0x00AB,
        NCXBUTTONUP = 0x00AC,
        NCXBUTTONDBLCLK = 0x00AD,
        INPUT_DEVICE_CHANGE = 0x00FE,
        INPUT = 0x00FF,
        KEYFIRST = 0x0100,
        KEYDOWN = 0x0100,
        KEYUP = 0x0101,
        CHAR = 0x0102,
        DEADCHAR = 0x0103,
        SYSKEYDOWN = 0x0104,
        SYSKEYUP = 0x0105,
        SYSCHAR = 0x0106,
        SYSDEADCHAR = 0x0107,
        UNICHAR = 0x0109,
        KEYLAST = 0x0108,
        IME_STARTCOMPOSITION = 0x010D,
        IME_ENDCOMPOSITION = 0x010E,
        IME_COMPOSITION = 0x010F,
        IME_KEYLAST = 0x010F,
        INITDIALOG = 0x0110,
        COMMAND = 0x0111,
        SYSCOMMAND = 0x0112,
        TIMER = 0x0113,
        HSCROLL = 0x0114,
        VSCROLL = 0x0115,
        INITMENU = 0x0116,
        INITMENUPOPUP = 0x0117,
        MENUSELECT = 0x011F,
        MENUCHAR = 0x0120,
        ENTERIDLE = 0x0121,
        MENURBUTTONUP = 0x0122,
        MENUDRAG = 0x0123,
        MENUGETOBJECT = 0x0124,
        UNINITMENUPOPUP = 0x0125,
        MENUCOMMAND = 0x0126,
        CHANGEUISTATE = 0x0127,
        UPDATEUISTATE = 0x0128,
        QUERYUISTATE = 0x0129,
        CTLCOLORMSGBOX = 0x0132,
        CTLCOLOREDIT = 0x0133,
        CTLCOLORLISTBOX = 0x0134,
        CTLCOLORBTN = 0x0135,
        CTLCOLORDLG = 0x0136,
        CTLCOLORSCROLLBAR = 0x0137,
        CTLCOLORSTATIC = 0x0138,
        MOUSEFIRST = 0x0200,
        MOUSEMOVE = 0x0200,
        LBUTTONDOWN = 0x0201,
        LBUTTONUP = 0x0202,
        LBUTTONDBLCLK = 0x0203,
        RBUTTONDOWN = 0x0204,
        RBUTTONUP = 0x0205,
        RBUTTONDBLCLK = 0x0206,
        MBUTTONDOWN = 0x0207,
        MBUTTONUP = 0x0208,
        MBUTTONDBLCLK = 0x0209,
        MOUSEWHEEL = 0x020A,
        XBUTTONDOWN = 0x020B,
        XBUTTONUP = 0x020C,
        XBUTTONDBLCLK = 0x020D,
        MOUSEHWHEEL = 0x020E,
        MOUSELAST = 0x020E,
        PARENTNOTIFY = 0x0210,
        ENTERMENULOOP = 0x0211,
        EXITMENULOOP = 0x0212,
        NEXTMENU = 0x0213,
        SIZING = 0x0214,
        CAPTURECHANGED = 0x0215,
        MOVING = 0x0216,
        POWERBROADCAST = 0x0218,
        DEVICECHANGE = 0x0219,
        MDICREATE = 0x0220,
        MDIDESTROY = 0x0221,
        MDIACTIVATE = 0x0222,
        MDIRESTORE = 0x0223,
        MDINEXT = 0x0224,
        MDIMAXIMIZE = 0x0225,
        MDITILE = 0x0226,
        MDICASCADE = 0x0227,
        MDIICONARRANGE = 0x0228,
        MDIGETACTIVE = 0x0229,
        MDISETMENU = 0x0230,
        ENTERSIZEMOVE = 0x0231,
        EXITSIZEMOVE = 0x0232,
        DROPFILES = 0x0233,
        MDIREFRESHMENU = 0x0234,
        IME_SETCONTEXT = 0x0281,
        IME_NOTIFY = 0x0282,
        IME_CONTROL = 0x0283,
        IME_COMPOSITIONFULL = 0x0284,
        IME_SELECT = 0x0285,
        IME_CHAR = 0x0286,
        IME_REQUEST = 0x0288,
        IME_KEYDOWN = 0x0290,
        IME_KEYUP = 0x0291,
        MOUSEHOVER = 0x02A1,
        MOUSELEAVE = 0x02A3,
        NCMOUSEHOVER = 0x02A0,
        NCMOUSELEAVE = 0x02A2,
        WTSSESSION_CHANGE = 0x02B1,
        TABLET_FIRST = 0x02c0,
        TABLET_LAST = 0x02df,
        CUT = 0x0300,
        COPY = 0x0301,
        PASTE = 0x0302,
        CLEAR = 0x0303,
        UNDO = 0x0304,
        RENDERFORMAT = 0x0305,
        RENDERALLFORMATS = 0x0306,
        DESTROYCLIPBOARD = 0x0307,
        DRAWCLIPBOARD = 0x0308,
        PAINTCLIPBOARD = 0x0309,
        VSCROLLCLIPBOARD = 0x030A,
        SIZECLIPBOARD = 0x030B,
        ASKCBFORMATNAME = 0x030C,
        CHANGECBCHAIN = 0x030D,
        HSCROLLCLIPBOARD = 0x030E,
        QUERYNEWPALETTE = 0x030F,
        PALETTEISCHANGING = 0x0310,
        PALETTECHANGED = 0x0311,
        HOTKEY = 0x0312,
        PRINT = 0x0317,
        PRINTCLIENT = 0x0318,
        APPCOMMAND = 0x0319,
        THEMECHANGED = 0x031A,
        CLIPBOARDUPDATE = 0x031D,
        DWMCOMPOSITIONCHANGED = 0x031E,
        DWMNCRENDERINGCHANGED = 0x031F,
        DWMCOLORIZATIONCOLORCHANGED = 0x0320,
        DWMWINDOWMAXIMIZEDCHANGE = 0x0321,
        GETTITLEBARINFOEX = 0x033F,
        HANDHELDFIRST = 0x0358,
        HANDHELDLAST = 0x035F,
        AFXFIRST = 0x0360,
        AFXLAST = 0x037F,
        PENWINFIRST = 0x0380,
        PENWINLAST = 0x038F,
        APP = 0x8000,
        USER = 0x0400,
        CPL_LAUNCH = USER + 0x1000,
        CPL_LAUNCHED = USER + 0x1001,
        SYSTIMER = 0x118,
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct CWPSTRUCT
    {
        public IntPtr lParam;
        public IntPtr wParam;
        public WindowsMessage message;
        public IntPtr hwnd;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct POINT
    {
        public int x, y;
        public POINT(int aX, int aY)
        {
            x = aX;
            y = aY;
        }
        public override string ToString()
        {
            return "(" + x + ", " + y + ")";
        }
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct MSG
    {
        public IntPtr hwnd;
        public WindowsMessage message;
        public IntPtr wParam;
        public IntPtr lParam;
        public ushort time;
        public POINT pt;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct RECT
    {
        public int Left, Top, Right, Bottom;

        public RECT(int left, int top, int right, int bottom)
        {
            Left = left;
            Top = top;
            Right = right;
            Bottom = bottom;
        }
        public override string ToString()
        {
            return "(" + Left + ", " + Top + ", " + Right + ", " + Bottom + ")";
        }
    }

    public delegate IntPtr HookProc(int code, IntPtr wParam, ref MSG lParam);
    public delegate bool EnumThreadDelegate(IntPtr Hwnd, IntPtr lParam);

    public static class Window
    {
        [DllImport("user32.dll")]
        public static extern bool EnumThreadWindows(uint dwThreadId, EnumThreadDelegate lpfn, IntPtr lParam);

        [DllImport("user32.dll", SetLastError = true)]
        public static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect);

        [DllImport("user32.dll")]
        public static extern bool IsWindowVisible(IntPtr hWnd);

        [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        static extern int GetClassName(IntPtr hWnd, System.Text.StringBuilder lpClassName, int nMaxCount);
        public static string GetClassName(IntPtr hWnd)
        {
            var sb = new System.Text.StringBuilder(256);
            int count = GetClassName(hWnd, sb, 256);
            return sb.ToString(0, count);
        }

        [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        static extern int GetWindowTextLength(IntPtr hWnd);
        [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder lpString, int nMaxCount);
        public static string GetWindowText(IntPtr hWnd)
        {
            int length = GetWindowTextLength(hWnd) + 2;
            var sb = new System.Text.StringBuilder(length);
            int count = GetWindowText(hWnd, sb, length);
            return sb.ToString(0, count);
        }
    }

    public static class WinAPI
    {
        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
        public static extern IntPtr GetModuleHandle(string lpModuleName);
        [DllImport("kernel32.dll")]
        public static extern uint GetCurrentThreadId();

        [DllImport("user32.dll", SetLastError = true)]
        public static extern IntPtr SetWindowsHookEx(HookType hookType, HookProc lpfn, IntPtr hMod, uint dwThreadId);
        [DllImport("user32.dll", SetLastError = true)]
        public static extern bool UnhookWindowsHookEx(IntPtr hhk);
        [DllImport("user32.dll")]
        public static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, ref MSG lParam);

        [DllImport("shell32.dll")]
        public static extern void DragAcceptFiles(IntPtr hwnd, bool fAccept);
        [DllImport("shell32.dll", CharSet = CharSet.Unicode)]
        public static extern uint DragQueryFile(IntPtr hDrop, uint iFile, System.Text.StringBuilder lpszFile, uint cch);
        [DllImport("shell32.dll")]
        public static extern void DragFinish(IntPtr hDrop);

        [DllImport("shell32.dll")]
        public static extern void DragQueryPoint(IntPtr hDrop, out POINT pos);
    }

    public static class UnityDragAndDropHook
    {
        public delegate void DroppedFilesEvent(List<string> aPathNames, POINT aDropPoint);
        public static event DroppedFilesEvent OnDroppedFiles;

        private static uint threadId;
        private static IntPtr mainWindow = IntPtr.Zero;
        private static IntPtr m_Hook;
        private static string m_ClassName = "UnityWndClass";

        [AOT.MonoPInvokeCallback(typeof(EnumThreadDelegate))]
        private static bool EnumCallback(IntPtr W, IntPtr _)
        {
            if (Window.IsWindowVisible(W) && (mainWindow == IntPtr.Zero || (m_ClassName != null && Window.GetClassName(W) == m_ClassName)))
            {
                mainWindow = W;
            }
            return true;
        }

        public static void InstallHook()
        {
            threadId = WinAPI.GetCurrentThreadId();
            if (threadId > 0)
                Window.EnumThreadWindows(threadId, EnumCallback, IntPtr.Zero);

            var hModule = WinAPI.GetModuleHandle(null);
            m_Hook = WinAPI.SetWindowsHookEx(HookType.WH_GETMESSAGE, Callback, hModule, threadId);
            WinAPI.DragAcceptFiles(mainWindow, true);
        }
        public static void UninstallHook()
        {
            WinAPI.UnhookWindowsHookEx(m_Hook);
            WinAPI.DragAcceptFiles(mainWindow, false);
            m_Hook = IntPtr.Zero;
        }

        [AOT.MonoPInvokeCallback(typeof(HookProc))]
        private static IntPtr Callback(int code, IntPtr wParam, ref MSG lParam)
        {
            if (code == 0 && lParam.message == WindowsMessage.DROPFILES)
            {
                WinAPI.DragQueryPoint(lParam.wParam, out POINT pos);

                uint n = WinAPI.DragQueryFile(lParam.wParam, 0xFFFFFFFF, null, 0);
                var sb = new System.Text.StringBuilder(1024);

                List<string> result = new();
                for (uint i = 0; i < n; i++)
                {
                    int len = (int)WinAPI.DragQueryFile(lParam.wParam, i, sb, 1024);
                    result.Add(sb.ToString(0, len));
                    sb.Length = 0;
                }
                WinAPI.DragFinish(lParam.wParam);
                OnDroppedFiles?.Invoke(result, pos);
            }
            return WinAPI.CallNextHookEx(m_Hook, code, wParam, ref lParam);
        }
    }
}

② FileDragAndDrop.cs

다음으로 위 스크립트를 응용하는 예시 코드를 작성했다.

매우 간단하다.

using UnityEngine;
using UnityEngine.Events;
using System.Collections.Generic;
using B83.Win32;

public class FileDragAndDrop : MonoBehaviour
{
    public UnityEvent onDroppedFiles; //파일이 드랍되었을 때 실행되는 이벤트
    public string[] filesDropped; //드랍된 파일들

    private void Awake()
    {
        UnityDragAndDropHook.InstallHook(); //모듈 설치
        UnityDragAndDropHook.OnDroppedFiles += OnDroppedFiles; //콜백 함수 등록
    }
    private void OnDestroy()
    {
        UnityDragAndDropHook.UninstallHook(); //모듈 제거
    }

    private void OnDroppedFiles(List<string> afiles, POINT aPos) //등록된 콜백 함수
    {
        filesDropped = afiles.ToArray(); //드랍된 파일들 갱신
        onDroppedFiles.Invoke(); //이벤트 실행
    }
}

 

이 두 개의 스크립트만 있으면 준비가 모두 완료된 것이다.


Inspector

이제 FileDragAndDrop 스크립트를 오브젝트에 부착해서 동작해보겠다.

 

게임 오브젝트를 하나 생성해주고,

※ 이름은 'FileDragAndDrop'이라고 지었다.

 

FileDragAndDrop 컴포넌트를 부착해준다.

그리고 게임을 실행한 뒤, Game 화면에 아무 파일을 드래그 앤 드롭 해본다.


▼ 결과.gif

잘 인식되는 것을 볼 수 있다.


Build를 할 때에는 Player Settings에서 Run In Background 옵션을 체크해주자.

파일을 드래그 하기 위해서는 잠시 외부로 Focus 되기 때문에 바로 반응이 안 따라와줄 수도 있다.

 

본 기능을 더 개선하여 특정 UI 범위에 들어왔을 때만 드랍되도록 할 수도 있다.

여기서 따로 구현은 안했지만 필요로 하다면, 관련 글을 또 쓰도록 하겠다.

 

마지막으로, 이를 활용하여 본인이 간단히 만든 프로그램을 소개하겠다.

바이러스는 절대 없으니, 필요하다면 안심하고 쓰길 바란다.

https://github.com/Giseung30/Text_File_Combiner

 

GitHub - Giseung30/Text_File_Combiner: 텍스트 파일들을 한번에 몰아보자

텍스트 파일들을 한번에 몰아보자. Contribute to Giseung30/Text_File_Combiner development by creating an account on GitHub.

github.com

댓글