How do I accept files to be opened via IDropTarget instead of on the command line?


Commenter Yaron wants to know how to use the new IDropTarget mechanism for receiving a list of files to open. (Also asked by Anthony Wieser as a comment to an article.) The MSDN documentation on Verbs and File Assocations mentions that DDE has been deprecated as a way of launching documents and that you should use the DropTarget method instead. But what is the DropTarget method? (Note that the word method here is in the sense of technique and not in the C++ sense of function that belongs to a class.)

The documentation in MSDN tells you what to do, but it does so very tersely. It says to create a DropTarget key under the verb key and create a Clsid string value whose data is the string version of the CLSID for your drop target. The documentation tells you to be careful in your IDropTarget::Drop, so it stands to reason that IDropTarget is the interface that will be used. From context, therefore, you should expect that the shell is going to simulate a drop on your drop target.

You can implement your drop target either as an in-process server or a local server. The in-process case is well-known; nearly all shell extensions are in-process. But using an in-process server for the DropTarget technique only solves half the problem: Sure, the IDropTarget::Drop will take place and you will get your IDataObject, but you still have to transfer the file list from your shell extension running inside the context menu host to your application. May as well let COM do the heavy lifting of marshalling the data. (Well, okay, maybe using COM is overkill. You might have a lighter weight way of getting the data across, but since that's out of scope for today's exercise, I'll leave it for you to figure out.)

Okay, let's roll up our sleeves and get to it! It turns out that nearly all the work is just creating a COM local server. If you know how to do that already, then I apologize in advance for the oppressive boredom about to fall upon you. I'll try to remember to wake you up when something interesting is about to happen. Note also that I am not an expert on COM local servers, so if you find a discrepancy between what I write and information from people who actually know what they're doing, go with the people who know what they're doing. (Actually, that sentence pretty much applies in general to everything I write.) Indeed, I had never written a COM local server before now, so all of what you see here is the result of a crash course in COM local servers from reading the documentation. (Translation: You could've done this too.)

Start by adding some header files and a forward reference.

#include <shlobj.h>
#include <shellapi.h>
#include <new> // for new(nothrow)

void OpenFilesFromDataObject(IDataObject *pdto);

Next, I'm going to steal the Process­Reference class which I had created some time ago. It's not the most efficient solution to the problem, but it works well enough, and it's a nice preparatory step in case a shell extension loaded into our process needs to take a process reference. We use the process reference object to keep track of our outstanding objects and locks.

ProcessReference *g_ppr;

Of course our custom drop target needs a class ID:

const CLSID CLSID_Scratch = { ... };

I leave it to you to fill in the CLSID structure from the output of uuidgen -s.

Next, our simple drop target. COM servers need to keep track of the number of objects that have been created, so we'll piggyback off our existing process reference.

class SimpleDropTarget : public IDropTarget
{
public:
 SimpleDropTarget() : m_cRef(1) { g_ppr->AddRef(); }
 ~SimpleDropTarget() { g_ppr->Release(); }

 // *** IUnknown ***
 STDMETHODIMP QueryInterface(REFIID riid, void **ppv)
 {
  if (riid == IID_IUnknown || riid == IID_IDropTarget) {
    *ppv = static_cast<IUnknown*>(this);
    AddRef();
    return S_OK;
  }
  *ppv = NULL;
  return E_NOINTERFACE;
 }

 STDMETHODIMP_(ULONG) AddRef()
 {
  return InterlockedIncrement(&m_cRef);
 }

 STDMETHODIMP_(ULONG) Release()
 {
  LONG cRef = InterlockedDecrement(&m_cRef);
  if (cRef == 0) delete this;
  return cRef;
 }

Next come the methods of IDropTarget, none of which are particularly interesting. We just say that we are going to copy the data.

 // *** IDropTarget ***
 STDMETHODIMP DragEnter(IDataObject *pdto,
    DWORD grfKeyState, POINTL ptl, DWORD *pdwEffect)
 {
  *pdwEffect &= DROPEFFECT_COPY;
  return S_OK;
 }

 STDMETHODIMP DragOver(DWORD grfKeyState,
   POINTL ptl, DWORD *pdwEffect)
 {
  *pdwEffect &= DROPEFFECT_COPY;
  return S_OK;
 }

 STDMETHODIMP DragLeave()
 {
  return S_OK;
 }

 STDMETHODIMP Drop(IDataObject *pdto, DWORD grfKeyState,
    POINTL ptl, DWORD *pdwEffect)
 {
  OpenFilesFromDataObject(pdto);
  *pdwEffect &= DROPEFFECT_COPY;
  return S_OK;
 }

private:
 LONG m_cRef;
};

People who know how COM servers work wake up: When something is dropped on our drop target, we call Open­Files­From­Data­Object. That's actually not all that interesting, but at least it's nontrivial. People who know how COM servers work can go back to sleep now.

The next part of the code is just setting up our class factory.

class SimpleClassFactory : public IClassFactory
{
public:
 // *** IUnknown ***
 STDMETHODIMP QueryInterface(REFIID riid, void **ppv)
 {
  if (riid == IID_IUnknown || riid == IID_IClassFactory) {
    *ppv = static_cast<IUnknown*>(this);
    AddRef();
    return S_OK;
  }
  *ppv = NULL;
  return E_NOINTERFACE;
 }

 STDMETHODIMP_(ULONG) AddRef()
 {
  return 2;
 }

 STDMETHODIMP_(ULONG) Release()
 {
  return 1;
 }

 // *** IClassFactory ***
 STDMETHODIMP CreateInstance(IUnknown *punkOuter, REFIID riid, void **ppv)
 {
    *ppv = NULL;
    if (punkOuter) return CLASS_E_NOAGGREGATION;
    SimpleDropTarget *pdt = new(nothrow) SimpleDropTarget();
    if (!pdt) return E_OUTOFMEMORY;
    HRESULT hr = pdt->QueryInterface(riid, ppv);
    pdt->Release();
    return hr;
 }

 STDMETHODIMP LockServer(BOOL fLock)
 {
  if (!g_ppr) return E_FAIL; // server shutting down
  if (fLock) g_ppr->AddRef(); else g_ppr->Release();
  return S_OK;
 }
};

SimpleClassFactory s_scf;

The Lock­Server call takes advantage of our process reference object by forwarding lock and unlock calls into the reference count of the process reference object. This keeps our process running until the server is unlocked.

Remember that COM rules specify that the class factory itself does not count as an outstanding COM object, so we don't use the same m_punkProcess trick that we did with our drop target. Instead, we just use a static object.

People who know how COM servers work wake up: The COM server code is pretty much done. Now we're back to user interface programming.

The next part of the code is just copied from our scratch program, with the following changes:

BOOL
OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
{
    g_hwndChild = CreateWindow(
        TEXT("listbox"), NULL, WS_CHILD | WS_VISIBLE | WS_TABSTOP,
        0, 0, 0,0, hwnd, (HMENU)1, g_hinst, 0);
    return TRUE;
}

The list box is not an important part of the program. We'll just fill it with data to prove that we actually did something.

void OpenFilesFromDataObject(IDataObject *pdto)
{
 if (!g_hwndChild) { /* need to create a new main window */ }
 FORMATETC fmte = { CF_HDROP, NULL, DVASPECT_CONTENT,
                    -1, TYMED_HGLOBAL };
 STGMEDIUM stgm;
 if (SUCCEEDED(pdto->GetData(&fmte, &stgm))) {
  HDROP hdrop = reinterpret_cast<HDROP>(stgm.hGlobal);
  UINT cFiles = DragQueryFile(hdrop, 0xFFFFFFFF, NULL, 0);
  for (UINT i = 0; i < cFiles; i++) {
   TCHAR szFile[MAX_PATH];
   UINT cch = DragQueryFile(hdrop, i, szFile, MAX_PATH);
   if (cch > 0 && cch < MAX_PATH) {
    ListBox_AddString(g_hwndChild, szFile);
   }
  }
  ReleaseStgMedium(&stgm);
 }
}

The Open­Files­From­Data­Object function does only enough work to prove that it actually got the list of file names. When we receive a data object from the simulated drop, we retrieve the HDROP and enumerate the files in it. For each file, we add it to the list box.

There's some code I've not bothered to write: Namely, if a request to open some files comes in after the user closed our main window, we need to open a new main window. (Exercise: How can this happen?)

Another difference between this program and real life is that in real life, your Open­Files­From­Data­Object would do some real work. But wait, if your function does any actual work, you should just AddRef the data object and return, so that the shell can return to interacting with the user. If you stop to do a lot of work before returning, the shell will lock up because it's waiting for your drop to complete.

// Version of OpenFilesFromDataObject that is more
// appropriate for real life.

void OpenFilesFromDataObject(IDataObject *pdto)
{
 if (!g_hwndChild) { /* need to create a new main window */ }
 pdto->AddRef();
 PostMessage(GetParent(g_hwndChild), WM_OPENFILES, 0,
             reinterpret_cast<LPARAM>(pdto));
}

case WM_OPENFILES:
 IDataObject *pdto = reinterpret_cast<IDataObject*>(lParam);
 ... rest of code from the original OpenFilesFromDataObject ...
 pdto->Release();
 break;

In real life, you just AddRef the data object and then post a message to finish processing it later. The aim here is to release the shell thread as quickly as possible. When the posted message is received, we can extract the information from the data object at our leisure. People who know how COM servers work can go back to sleep now.

Finally, we hook up our class factories to the main program:

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev,
                   LPSTR lpCmdLine, int nShowCmd)
{
    MSG msg;
    HWND hwnd;

    g_hinst = hinst;

    if (!InitApp()) return 0;

    if (SUCCEEDED(CoInitialize(NULL))) {/* In case we use COM */
        HRESULT hrRegister;
        DWORD dwRegisterCookie;

        {
            ProcessReference ref;
            g_ppr = &ref;

            hrRegister = CoRegisterClassObject(CLSID_Scratch, &s_scf,
                  CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE,
                  &dwRegisterCookie);

            hwnd = CreateWindow(
                TEXT("Scratch"),                /* Class Name */
                TEXT("Scratch"),                /* Title */
                WS_OVERLAPPEDWINDOW,            /* Style */
                CW_USEDEFAULT, CW_USEDEFAULT,   /* Position */
                CW_USEDEFAULT, CW_USEDEFAULT,   /* Size */
                NULL,                           /* Parent */
                NULL,                           /* No menu */
                hinst,                          /* Instance */
                0);                             /* No special parameters */

            if (CompareStringA(LOCALE_INVARIANT, NORM_IGNORECASE,
                 lpCmdLine, -1, "-Embedding", -1) != CSTR_EQUAL &&
                CompareStringA(LOCALE_INVARIANT, NORM_IGNORECASE,
                 lpCmdLine, -1, "/Embedding", -1) != CSTR_EQUAL) {
                /* OpenFilesFromCommandLine(); */
            }

            ShowWindow(hwnd, nShowCmd);

            while (GetMessage(&msg, NULL, 0, 0)) {
                TranslateMessage(&msg);
                DispatchMessage(&msg);
            }
            g_hwndChild = NULL;

        } // wait for process references to die
        g_ppr = NULL;

        if (SUCCEEDED(hrRegister)) {
            CoRevokeClassObject(dwRegisterCookie);
        }
        CoUninitialize();
    }

    return 0;
}

After creating our process reference, we register our class factory by calling Co­Register­Class­Object. We do this even if not invoked by COM, because we want COM to be able to find us once we're up and running: If the user runs the application manually and then double-clicks an associated document, we want that document to be handed to us rather than having COM launch another copy of the program.

After creating the window, we check if the command line is -Embedding or /Embedding. This is the magic command line switch which COM gives us when we are being launched as a local server. If we don't have that switch, then we're being launched with a file name on our command line, so proceed with "old school" command line parsing. (I didn't bother writing the Open­Files­From­Command­Line function since it is irrelevant to the topic.)

After our message loop exits, we clear the g_hwndChild so Open­Files­From­Data­Object knows that there is no main window any more. In real life, we'd have to create a new main window and restart the message loop.

Once all outstanding COM objects and server locks and process references are gone, we can tear down the process. We unregister the COM server (if we registered it) so that COM won't try to ask us to open any more documents. (COM will instead launch a new copy of the program.)

And that's it.

Oh wait, we also have to register this program so COM and the shell can find us.

Registering the COM server is just a matter of setting the registry key

[HKCR\CLSID\{...}\LocalServer32]
@="C:\\Path\\To\\scratch.exe"

You probably should also set a friendly name into HKCR\CLSID\{...} so people will have a clue what your server is for.

People who know how COM servers work wake up: After we register our class with COM, we can register it with the shell. For demonstration purposes, we'll make our command a secondary verb on text files.

[HKCR\txtfile\shell\scratch\DropTarget]
"Clsid"="{...}"

Wow, all we had to do was set two registry values and boom, we can now accept files via drop target. Multiselect a whole bunch of text files, right-click them, and then select "scratch". The shell sees that the verb is registered as a drop target, so it calls Co­Create­Instance on the CLSID you provided. COM looks up the CLSID in the registry and finds the path to your program. Your program runs with the -Embedding flag, registers its class factory, and awaits further instructions. COM asks your class factory to create a drop target, which it returns to the shell. The shell then performs the simulated drop, and when you get the IDropTarget::Drop, your code springs into action and extracts all the files in the data object.

Now that we have all this working, it's just one more tiny step to register your application's drop target so that it is invoked when the user drops a group of files on the EXE itself (or on a shortcut to the EXE):

[HKLM\Software\Microsoft\Windows\CurrentVersion\App Paths\scratch.exe]
"DropTarget"="{...}"

With this additional registration, grab that bunch of files and drop it on the scratch.exe icon. Instead of passing all those files on the command line (and possibly overflowing the command line limit), the shell goes through the same procedure as it did with the context menu to hand the list of files to your program via the data object.

Nearly all of the work here was just managing the COM local server. The parts that had to do with the shell were actually quite small.

Comments (24)
  1. Anonymous says:

    I have a few questions about a simulated drop (from both sides of the interface).

    Is there a way for the drop target to detect a simulated drop (vs a real drop with the mouse)?

    When the shell simulates a drop, does it call BOTH DragEnter and DragOver? What does it pass for key state, mouse point and effect?

    If I have to simulate a drop (let’s say I want to copy a file to an arbitrary IShellFolder), am I required to call DragEnter? What about DragOver? What mouse point to use?

  2. Anonymous says:

    Is there a way for the drop target to detect a simulated drop (vs a real drop with the mouse)?

    It wouldn’t be a very good simulation if there was!

    When the shell simulates a drop, does it call BOTH DragEnter and DragOver? What does it pass for key state, mouse point and effect?

    In the spirit of Raymond, suck it and see! You could implement these interface members, and use OutputDebugString to check.  That’s my usual approach to these things.

    As for you doing it… if you don’t want other programs detecting you’re a fake, it would be best to be as thorough as possible.

    Iain.

  3. Anonymous says:

    And if I drop a file named “-Embedding”?

    [Then the file is opened. (Think about it.) -Raymond]
  4. Anonymous says:

    And if I drop a file named "-Embedding"?

    And if I want to do it in assembly?

    Seriously, what drives such questions?

  5. Anonymous says:

    @Iain:

    It wouldn’t be a very good simulation if there was!

    Let’s say I’m writing a namespace extension. I want to detect when you drop a file at specific coordinates, or when you do a "paste" operation. For the first I have to use the mouse position, and for the second I have to ignore it. Would be nice to know which is the case.

    In the spirit of Raymond, suck it and see! You could implement these interface members, and use OutputDebugString to check.  That’s my usual approach to these things.

    I can argue that this is contrary to the "Raymond spirit". This way I will start depending on some undocumented behavior, possibly subject to change in the next version. Imagine if DragOver is being called. I do some processing there instead of in DragEnter/Drop. Well, Windows 11 may stop calling it and I’m screwed. If the actual sequence is documented, I can write my code correctly and future-proof it. Or maybe to create a shortcut, the shell is telling me that both Shift and Ctrl are pressed, instead of sending DROPEFFECT_LINK (for compatibility with some popular accounting software). Depending on such observed, but undocumented behavior is dangerous and should be avoided if there is an alternative.

    As for you doing it… if you don’t want other programs detecting you’re a fake, it would be best to be as thorough as possible.

    I don’t care if the program detects that I’m a fake (in fact it may be beneficial because of my first point). I want to paste a file (IDataObject) into a folder (IShellFolder with IDropTarget). There are multiple sequences I can use, but the only one that is guaranteed to work is the one that shell is using. That’s because (I’m assuming) the drop target has been tested with the shell’s fake drop. So the closer my behavior is to the shell, the better chances I have to not break anything.

  6. Anonymous says:

    >> And if I drop a file named “-Embedding”?

    >

    > Seriously, what drives such questions?

    I’m not sure if this was an attempt at knocking the technique or a legitimate question – on Unix systems, filenames that look like switches is a problem that comes up often enough (especially because the shell expands wildcards, so the user might not even be aware there’s such a file being passed on the command line) that programs often have a special switch (“–“) to indicate that everything that follows is a filename even if it looks like a switch/option.

    [The shell doesn’t pass relative paths (what would it be relative to?) so the question is moot. That’s why I wrote “think about it.” -Raymond]
  7. Anonymous says:

    Great post Raymond. I’m just wondering about one thing, in this line

    HDROP hdrop = reinterpret_cast<HDROP>(stgm.hGlobal);

    shouldn’t you use GlobalLock? Or does the shell guarantee its DataObject won’t bother you with ancient relics like movable memory blocks? If so, is that true for all DataObjects one gets from the shell?

    [The person who looks inside the HDROP must GlobalLock it as appropriate. But we’re not looking inside it here. We’re just operating on the opaque handle. -Raymond]
  8. Anonymous says:

    “[HKLMSoftwareMicrosoftWindowsCurrentVersionApp Pathsscratch.exe]

    And if this is a per user app with its COM stuff in HKCU? AFAIK App Paths is HKLM only for some stupid reason, no UAC-less install for you!

    [Sorry the solution isn’t 100% applicable to all situations. But at least it helps the other 99% of the programs out there. I like to think that’s worth something. -Raymond]
  9. Anonymous says:

    I know this is not provided as 100% tested bug-free code, but for the sake of people who will undoubtedly copy and paste it, shouldn’t there be a test for Query failure; in the code:

       HRESULT hr = pdt->QueryInterface(riid, ppv);

       pdt->Release();

    ?

    I know that it’s unlikely you will get queried about another interface not supported, but still, I think that Release() should be conditional.

    [The code is correct. If the QI fails, then hr is set to E_NOINTERFACE and the SimpleDropTarget is destroyed. -Raymond]
  10. Anonymous says:

    Although I love articles about the deep stuff, I think articles like this are fine too once in a while, especially for people who are more susceptible to a hand on approach than to reading the docs (although they still should after they roughly understand what they’re doing). Besides, I’ve ran into command line length limits in the past, so this may come in handy someday, and given that the COM stuff can be handled by templates or a framework, I think this is actually easier than the antiquated command line interface.

    Seriously, it’s 2010, console support should be dropped. If people really want a console they can code one up themselves. ;-)

  11. Anonymous says:

    I’ll try to remember to wake you up when something interesting is about to happen.

    That and the part you tell people sleep and wake now makes me giggle. :)

  12. Anonymous says:

    This sample should have been documented in the SDK 15 years ago. Why isn’t it?

    [Um, the feature didn’t exist 15 years ago. -Raymond]
  13. Anonymous says:

    Amazing! I was just working on this very problem. Thanks Raymond.

  14. Anonymous says:

    Pierre B: *ppv, not pdt, is set to the result of QueryInterface.  The caller to CreateInstance owns *ppv if it is set, not us.  We own pdt, so need to release it before return.

  15. Anonymous says:

    Maybe it’s nitpicking, but isn’t OleInitialize() required instead of CoInitialize() when messing with IDataObject?

  16. Anonymous says:

    I’m dubious about the App Paths registry entry..

    What if two programs in the system are named scratch.exe?

    Wouldn’t that registry entry redirect launching of all scratch.exe programs (target of file dropping) to launching the LocalServer32 entry instead?

    (actually I tested a bit, and indeed it seems to be affecting file dropping on all scratch.exe)

    You’d better choose carefully the name of your executable then..

    Why is there no way for the executable itself (before executing) to tell the shell that it supports DropTarget? (for example, a special resource entry)

  17. Anonymous says:

    > [Um, the feature didn’t exist 15 years ago. -Raymond]

    You are probably lying, but let’s pretend your not. Then replace 15 with 14 or “very long time ago”.

    [Support for DropTarget-based execution was added in Windows XP, much less than 15 years ago. And all the information in the article is documented; you just need to connect the dots. Sorry there isn’t a sample for every possible way of connecting dots. Sometimes you just need to connect them yourself. That’s why it’s called programming. -Raymond]
  18. Anonymous says:

    You should invent and patent a markup language for articles like this. That would be a great tool to have in many technical works where the authors have only a foggy idea of who their target audience is*. Imagine an ebook transforming itself to your skillset. Brilliant.

    * What you saying? That authors should figure out what "target audience" means? Don’t be silly, that’s HARD WORK!

  19. Anonymous says:

    "You are probably lying"? I guess blogs get the audience they deserve — if the host self-diagnoses as having the social skills of a thermonuclear device, he’ll get likewise readers.

  20. Anonymous says:

    @JM

    Having the social skills of a thermonuclear device does not imply that one is an idiot. Raymond clearly is not, while yourself and sample are.

  21. Anonymous says:

    Nice, thanks!

    Questions, please?

    How do I know that I should use this cast: reinterpret_cast<HDROP>(stgm.hGlobal)?

    Is it OK to use it if tymed is not TYMED_HGLOBAL? Or Is tymed guaranteed to be TYMED_HGLOBAL when IDataObject is coming from IDropTarget::Drop? Or does DragQueryFile know how to handle random hDrop? Or…?

    [Exercise: Study the rules for IDataObject, then apply those rules to the situation here. -Raymond]
  22. Anonymous says:

    [Exercise: Study the rules for IDataObject, then apply those rules to the situation here. -Raymond]

    Can anyone play this game?  The FORMATETC tells you how GetData should pass back the HDROP.  You specified TYMED_HGLOBAL, so the HDROP was given back in a HGLOBAL.  If you’d asked for something from GetData that can’t fit in an HGLOBAL, it should return DV_E_FORMATETC.  (In general, that is.  I don’t know why you’d be asking for anything larger in the case under discussion.)

  23. Anonymous says:

    Oh, and I forgot to say:  The documentation for GetData says that you can bitwise or multiple TYMED_* types, and the callee can pick which type to give back.  I didn’t realize that before this, and it means that you would have to check what GetData returned in stgm.tymed.  I wonder how many implementations of IDataObject don’t handle that nuance?

    Raymond, that little bit of knowledge alone makes the entire article worthwhile.  Thanks!

  24. Anonymous says:

    @The_Assimilator: In my book, "you’re lying" is about the worst thing you could say to a technical person, as it implies they’re putting their ego before their profession. I’m an idiot for calling someone out for being rude to the author of the blog in an unnecessary and unlikely manner? I’m not sure how that works, but I’ll choose to believe you just misunderstood what I wrote. Either that or your opinion is based on previous experience, in which case due apologies for whatever I did in the past…

    In any case, let’s drop it, I already regret commenting at all.

Comments are closed.