본문 바로가기
[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

반응형

댓글