Hello, I have a need to extend unity to allow drag and dropping of files from the OS (preferably both OSX and Windows) into my unity application. To enable this, I decided to use System.Windows.Forms approach rather than GTK or other interfaces. With this library, My strategy is this:
- Copy the System.Windows.Forms.dll from C:\Program Files (x86)\Unity\Editor\Data\Mono\lib\mono\2.0 into my Plugins folder (Unity Pro)
- Create code to find the application window that I want to allow drag and drop on (this works)
- Create an invisible form over the top of the application window (Current error)
- On drag and drop events on the invisible form, send file information to my application for processing
My current struggle with this code is the instantiation of the Form class I created that is a subclass of the System.Windows.Forms.Form. The error I receive is:
NotImplementedException: The requested feature is not implemented.
System.Runtime.InteropServices.Marshal.ReadInt16 (System.Object ptr, Int32 ofs)
System.Drawing.Font.FromLogFont (System.Object lf, IntPtr hdc)
System.Drawing.Font.FromHfont (IntPtr hfont)
System.Drawing.SystemFonts.get_DefaultFont ()
System.Windows.Forms.Theme…ctor ()
System.Windows.Forms.ThemeWin32Classic…ctor ()
System.Windows.Forms.ThemeEngine…cctor ()
Rethrow as TypeInitializationException: An exception was thrown by the type initializer for System.Windows.Forms.ThemeEngine
System.Windows.Forms.SystemInformation.get_MenuAccessKeysUnderlined ()
System.Windows.Forms.Control…ctor ()
(wrapper remoting-invoke-with-check) System.Windows.Forms.Control:.ctor ()
System.Windows.Forms.WindowsFormsSynchronizationContext…cctor ()
Rethrow as TypeInitializationException: An exception was thrown by the type initializer for System.Windows.Forms.WindowsFormsSynchronizationContext
System.Windows.Forms.Control…ctor ()
System.Windows.Forms.ScrollableControl…ctor ()
System.Windows.Forms.ContainerControl…ctor ()
System.Windows.Forms.Form…ctor ()
DragDropWindow+InvForm…ctor (Rectangle rect)
(wrapper remoting-invoke-with-check) DragDropWindow/InvForm:.ctor (System.Drawing.Rectangle)
DragDropWindow…ctor (System.String processName) (at Assets/Scripts/GUI/Components/General/DragDrop.cs:147)
I am currently testing this on a Windows system with Unity Pro version 3.5.4. From my understanding, Unity has been based off of Mono 2.6.x since 3.x and System.Windows.Forms has been functional in Mono since 1.x.
Here’s my code:
using UnityEngine;
using System.Collections;
using System.Windows.Forms;
using System.IO;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Diagnostics;
public class DragDropWindow {
private bool _allowDrop = true;
private InvForm _form;
private Vector2 _dropPoint;
private Rectangle _rectangle;
private bool _foundWindow = false;
private int _processID;
private delegate bool EnumWindowCallback(System.IntPtr hWnd, System.IntPtr lParam);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool GetWindowRect(HandleRef hWnd, out RECT lpRect);
[DllImport("user32.dll")]
static extern bool EnumWindows(EnumWindowCallback callback, System.IntPtr extraData);
[DllImport("user32.dll")]
public static extern int GetWindowThreadProcessId(HandleRef handle, out int processId);
[StructLayout(LayoutKind.Sequential)]
public struct RECT {
public int Left; // x position of upper-left corner
public int Top; // y position of upper-left corner
public int Right; // x position of lower-right corner
public int Bottom; // y position of lower-right corner
}
/* Windows Form class
*/
public class InvForm : Form
{
private Rectangle _rectangle;
private System.ComponentModel.IContainer components = null;
public InvForm(Rectangle rect)
{
_rectangle = rect;
}
protected override void Dispose(bool disposing)
{
if (disposing && components != null)
{
components.Dispose();
}
base.Dispose(disposing);
}
/* load procedure for the invisible windows form
*/
private void InvForm_Load(object sender, System.EventArgs e)
{
this.AllowDrop = true;
this.Bounds = _rectangle;
this.DragEnter += InvDragEnter;
this.DragDrop += InvDragDrop;
}
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.Text = "InvForm";
}
//called by the form
private void InvDragEnter(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.FileDrop))
{
e.Effect = DragDropEffects.Copy;
}
else
{
e.Effect = DragDropEffects.None;
}
}
//called by the form
private void InvDragDrop(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.FileDrop))
{
string[] filePaths = (string[])(e.Data.GetData(DataFormats.FileDrop));
foreach (string fileLoc in filePaths)
{
// Code to read the contents of the text file
if (File.Exists(fileLoc))
{
UnityEngine.Debug.Log(fileLoc);
/*using (TextReader tr = new StreamReader(fileLoc)) {
MessageBox.Show(tr.ReadToEnd());
}*/
}
}
}
}
}
/* Initialize the drag and drop window facade class.
* The parameter is the process name of the application process on which to attach drag and
* drop functionality
*/
public DragDropWindow(string processName) {
_rectangle = new Rectangle();
//get the Windows form handle of the desired unity application
//Process[] processes = Process.GetProcessesByName(processName); // example: "Unity"
Process[] processes = Process.GetProcesses();
foreach (Process p in processes){
if (!p.HasExited) {
try {
if (p.ProcessName == processName) {
_processID = p.Id;
//unfortunately, no can be told what the main window handle is... you have to see it for yourself.
System.IntPtr dummy = (System.IntPtr)0;
if (!EnumWindows(GetWindowHandle, dummy)) {
UnityEngine.Debug.Log("Enumeration of Windows failed");
}
//initialize the invisible window that will sit on top of the requeste application
if (_foundWindow) {
UnityEngine.Debug.Log("the dimensions of the requested application's window: " + _rectangle.ToString());
//launch the invisible form
// CURRENT ERROR ==========================================================================
_form = new InvForm(_rectangle);
// END CURRENT ERROR ======================================================================
System.Windows.Forms.Application.Run(_form);
}
break;
}
} catch (System.InvalidOperationException e) {
//UnityEngine.Debug.Log("Process does not exist");
}
}
}
}
/* callback to gather the window handles for the specified processes
* this callback is of type EnumWindowCallback.
*/
public bool GetWindowHandle(System.IntPtr hWnd, System.IntPtr lParam){
RECT rct;
//attempt to get the unity main window bounds
if (!GetWindowRect(new HandleRef(this, hWnd), out rct)) {
UnityEngine.Debug.Log("Can't get Window");
return true;
}
int num = 0;
GetWindowThreadProcessId(new HandleRef(this, hWnd), out num);
if (num == _processID){
if(rct.Right - rct.Left + 1 > 100 && rct.Bottom - rct.Top + 1 > 100){
_rectangle.X = rct.Left;
_rectangle.Y = rct.Top;
_rectangle.Width = rct.Right - rct.Left + 1;
_rectangle.Height = rct.Bottom - rct.Top + 1;
_foundWindow = true;
}
}
return true;
}
}
Thank you for any help on this problem. I, of course, would love to hear if there are better ways of enabling this functionality.
I found the simple answer, just use the .NET 2.0 System.windows.forms.dll rather than mono’s System.windows.forms.dll. This change addressed the current problem but it appears there are a couple more issues to take care of inorder to make this work (make the window passive to user interaction, form staying on top of the unity window, and ensuring the new form never over takes the OS context from the unity app. None of these are addressed in the above code.