imgui icon indicating copy to clipboard operation
imgui copied to clipboard

Drag & Drop files using Win32 and Dear ImGui

Open johanwendin opened this issue 6 years ago • 5 comments

Drag & Drop files using Win32 and Dear ImGui

Whilst working on my editor I ran into the desire to be able to drag-n-drop files from within Windows into my editor, which is based on Dear ImGui.

Now, I could take the easy route and not care where the user drops the file (just that some files were dropped), doing so would have been as easy as:

//--- prior to main loop:
DragAcceptFiles(hwnd, TRUE);

//--- during main loop, in the win32 window event handler:
case WM_DROPFILES: {
	// do something useful with wParam
	...
	return 0;
} break;

//--- post main loop:
DragAcceptFiles(hwnd, FALSE);

... and you're done.

But, I wanted to be able to utilize the excellent drag-drop functionality within Dear ImGui; tooltips, highlighting drop targets and filtering out where the user can drop what kind of files.

It took some time (and some help from Omar) to get this working, so I promised him a mini-tutorial of how I did it. If you see any glaring errors or omissions, please let me know!

The first parts are easy enough, instead of using DragAcceptFiles and WM_DROPFILES, you have to do this: (parts left as an excercise for the reader are marked with ...)

#include <oleidl.h>

// create a class inheriting from IDropTarget
class DropManager : public IDropTarget
{
public:
	//--- implement the IUnknown parts
	// you could do this the proper way with InterlockedIncrement etc,
	// but I've left out stuff that's not exactly necessary for brevity
	ULONG AddRef()  { return 1; }
	ULONG Release() { return 0; }

	// we handle drop targets, let others know
	HRESULT QueryInterface(REFIID riid, void **ppvObject)
	{
		if (riid == IID_IDropTarget)
		{
			*ppvObject = this;	// or static_cast<IUnknown*> if preferred
			// AddRef() if doing things properly
                        // but then you should probably handle IID_IUnknown as well;
			return S_OK;
		}

		*ppvObject = NULL;
		return E_NOINTERFACE;
	};


	//--- implement the IDropTarget parts

	// occurs when we drag files into our applications view
	HRESULT DragEnter(IDataObject *pDataObj, DWORD grfKeyState, POINTL pt, DWORD *pdwEffect)
	{
		// TODO: check whether we can handle this type of object at all and set *pdwEffect &= DROPEFFECT_NONE if not;

		// do something useful to flag to our application that files have been dragged from the OS into our application
		...

		// trigger MouseDown for button 1 within ImGui
		...

		*pdwEffect &= DROPEFFECT_COPY;
		return S_OK;
	}

	// occurs when we drag files out from our applications view
	HRESULT DragLeave() { return S_OK; }

	// occurs when we drag the mouse over our applications view whilst carrying files (post Enter, pre Leave)
	HRESULT DragOver(DWORD grfKeyState, POINTL pt, DWORD *pdwEffect)
	{
		// trigger MouseMove within ImGui, position is within pt.x and pt.y
		// grfKeyState contains flags for control, alt, shift etc
		...

		*pdwEffect &= DROPEFFECT_COPY;
		return S_OK;
	}

	// occurs when we release the mouse button to finish the drag-drop operation
	HRESULT Drop(IDataObject *pDataObj, DWORD grfKeyState, POINTL pt, DWORD *pdwEffect)
	{
		// grfKeyState contains flags for control, alt, shift etc

		// render the data into stgm using the data description in fmte
		FORMATETC fmte = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
		STGMEDIUM stgm;

		if (SUCCEEDED(pDataObj->GetData(&fmte, &stgm)))
		{
			HDROP hdrop = (HDROP)stgm.hGlobal; // or reinterpret_cast<HDROP> if preferred
			UINT file_count = DragQueryFile(hdrop, 0xFFFFFFFF, NULL, 0);

			// we can drag more than one file at the same time, so we have to loop here
			for (UINT i = 0; i < file_count; i++)
			{
				TCHAR szFile[MAX_PATH];
				UINT cch = DragQueryFile(hdrop, i, szFile, MAX_PATH);
				if (cch > 0 && cch < MAX_PATH)
				{
					// szFile contains the full path to the file, do something useful with it
					// i.e. add it to a vector or something
				}
			}

			// we have to release the data when we're done with it
			ReleaseStgMedium(&stgm);

			// notify our application somehow that we've finished dragging the files (provide the data somehow)
			...
		}

		// trigger MouseUp for button 1 within ImGui
		...

		*pdwEffect &= DROPEFFECT_COPY;
		return S_OK;
	}
}

You will need to initialize Ole, instantiate an object of the above class and register it:

int APIENTRY WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
    OleInitialize(NULL);

    HWND hwnd = ...   // open your window here

    DropManager dm;
    RegisterDragDrop(hwnd, &dm);

    // main-loop goes here
    ...

    // post main-loop
    RevokeDragDrop(hwnd);

    // clean up your stuff here
    ...

    OleUninitialize();
    return 0;
}

The reason why we're triggering MouseDown, MouseUp and MouseMove is because - unless you're using polling - your application will not receive any WM_-messages regarding mouse input (key input etc will still be received) during the drag-drop operation.

Also, if you're like me and using PeekMessage instead of GetMessage, your PeekMessage call has to handle both window messages and thread messages (the IDropTarget-stuff seem to be handled by thread messages) so your:

MSG message;
while (PeekMessage(&message, hwnd, 0, 0, PM_REMOVE)) {
	TranslateMessage(&message);
	DispatchMessage(&message);
}

has to be changed to:

MSG message;
while (PeekMessage(&message, NULL, 0, 0, PM_REMOVE)) {
	TranslateMessage(&message);
	DispatchMessage(&message);
}

Now, within your application, you have somehow marked that you are dragging files and should begin utilizing Dear ImGui's drag-drop functionality.

if (isDraggingFiles) // somehow set to true when dragging files on top of your application
{
	if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceExtern))	// we use an external source (i.e. not ImGui-created)
	{
		// replace "FILES" with whatever identifier you want - possibly dependant upon what type of files are being dragged
		// you can specify a payload here with parameter 2 and the sizeof(parameter) for parameter 3.
		// I store the payload within a vector of strings within the application itself so don't need it.
		ImGui::SetDragDropPayload("FILES", nullptr, 0);
		ImGui::BeginTooltip();
		ImGui::Text("FILES");
		ImGui::EndTooltip();
		ImGui::EndDragDropSource();
	}
}

and on the receiving end:

if (ImGui::BeginDragDropTarget())
{
	if (ImGui::AcceptDragDropPayload("FILES"))  // or: const ImGuiPayload* payload = ... if you sent a payload in the block above
	{
		// draggedFiles is my vector of strings, how you handle your payload is up to you
		for (const auto &file : draggedFiles)
		{
			// do something with file
			...
		}
	}

	ImGui::EndDragDropTarget();
}

BeginDragDropSource has to be called the frame where the IDropTarget::Drop gets called, so if you're doing stuff similar to me, do not set isDraggingFiles to false before the BeginDragDropTarget section to make sure BeginDragDropSource gets called.

Hopefully this should be enough to get you going with dragging and dropping files from windows into your Dear ImGui-application.

Good luck!

/Johan

johanwendin avatar May 31 '19 20:05 johanwendin

It should be noted that triggering MouseDown interacts a bit wonky with windows that doesn’t have the ImGuiWindowFlags_NoMove set. I get around it by always using that flag. :)

Another way would perhaps be to disable window-moving while dragging. How to do that is left as an exercise for whoever reads this comment! ;)

johanwendin avatar May 31 '19 21:05 johanwendin

Hi Johan, thanks for the nice write up. I am working on a similar problem and I have the drop target interface working. Two questions:

  1. How do I report the mouse events correctly to Dear ImGui?
  2. How do I correctly query if a window is hovered? E.g. my accepting window could be hovered but be hidden under another window.

Another thing that is currently not entirely clear to me is how the cursor effect of OLE should be handled. Does ImGui overwrite the cursor or do I still need to set this properly from IDropTarget callbacks?

Thanks, -Dirk

dgregorius avatar Jan 27 '20 00:01 dgregorius

How do I report the mouse events correctly to Dear ImGui?

Update ImGui::GetIO().Mouse* fields once per frame (or less often if input state does not change). See imgui_impl_win32.cpp for an example. Mouse coordinates are relative to top-left corner of main viewport or are absolute screen coordinates if you are using docking branch.

How do I correctly query if a window is hovered? E.g. my accepting window could be hovered but be hidden under another window.

You may use ImGui::IsWindowHovered() to check whether current window is hovered. This API has to be called between Begin() and End() calls.

rokups avatar Jan 27 '20 07:01 rokups

Thanks! That is easy enough. I am using the GLFW backend which on the first look seems to poll mouse coordinates. Not sure if GLFW caches events or uses some async function. I will figure this out.

I am aware of ImGui::IsWindowHovered(). Unfortunately I am in the IDropTarget callback when I need to resolve tjhis. Is there a way to query whether a window was hovered in the last frame? I can compare the window in question with hovered window in the context I guess. Or I can cache that of course myself. I am wondering what the professional ImGUI programmer and by there mothers recommended solution is?

dgregorius avatar Jan 27 '20 22:01 dgregorius

This is so helpful! Thanks so much, I was just trying to incorporate this functionality into my renderer at https://github.com/Anton-Os/Topl.git

Anton-Os avatar May 08 '24 17:05 Anton-Os