Dragging a shell object, part 1: Getting the IDataObject


The shell gives you the IDataObject; all you have to do is drag it around. (This is the first of a five-part series.)

Start with the scratch program, and add the function GetUIObjectOfFile from an earlier article. Also, change the calls to CoInitialize and CoUninitialize to OleInitialize and OleUninitialize, respectively, since we’re now going to be using full-on OLE and not just COM.

In order to initiate a drag/drop operation, we need a drop source:

class CDropSource : public IDropSource
{
public:
  // *** IUnknown ***
  STDMETHODIMP QueryInterface(REFIID riid, void **ppv);
  STDMETHODIMP_(ULONG) AddRef();
  STDMETHODIMP_(ULONG) Release();

  // *** IDropSource ***
  STDMETHODIMP QueryContinueDrag(BOOL fEscapePressed, DWORD grfKeyState);
  STDMETHODIMP GiveFeedback(DWORD dwEffect);

  CDropSource() : m_cRef(1) { }
private:
  ULONG m_cRef;
};

HRESULT CDropSource::QueryInterface(REFIID riid, void **ppv)
{
  IUnknown *punk = NULL;
  if (riid == IID_IUnknown) {
    punk = static_cast<IUnknown*>(this);
  } else if (riid == IID_IDropSource) {
    punk = static_cast<IDropSource*>(this);
  }

  *ppv = punk;
  if (punk) {
    punk->AddRef();
    return S_OK;
  } else {
    return E_NOINTERFACE;
  }
}

ULONG CDropSource::AddRef()
{
  return ++m_cRef;
}

ULONG CDropSource::Release()
{
  ULONG cRef = --m_cRef;
  if (cRef == 0) delete this;
  return cRef;
}

HRESULT CDropSource::QueryContinueDrag(
          BOOL fEscapePressed, DWORD grfKeyState)
{
  if (fEscapePressed) return DRAGDROP_S_CANCEL;

  // [Update: missing paren repaired, 7 Dec]
  if (!(grfKeyState & (MK_LBUTTON | MK_RBUTTON)))
    return DRAGDROP_S_DROP;

  return S_OK;
}

HRESULT CDropSource::GiveFeedback(DWORD dwEffect)
{
  return DRAGDROP_S_USEDEFAULTCURSORS;
}

As you can see, this drop source is extraordinarily boring. Even the interesting methods are uninteresting.

The IDropSource::QueryContinueDrag method is pretty much boilerplate. If the Escape key was pressed, then cancel the drag/drop operation. If the mouse buttons are released, then complete the operation. Otherwise, continue the operation.

The IDropSource::GiveFeedback method is even less interesting. It merely returns DRAGDROP_S_USEDEFAULTCURSORS to indicate that it wants default drag feedback.

Believe it or not, we now have everything we need to drag a file.

void OnLButtonDown(HWND hwnd, BOOL fDoubleClick,
                   int x, int y, UINT keyFlags)
{
  IDataObject *pdto;
  // In a real program of course
  // you wouldn't use a hard-coded path.
  // [comment added 11am because apparently some
  // people thought this wasn't self-evident.]
  if (SUCCEEDED(GetUIObjectOfFile(hwnd,
                    L"C:\\Windows\\clock.avi",
		    IID_IDataObject, (void**)&pdto))) {
    IDropSource *pds = new CDropSource();
    if (pds) {
      DWORD dwEffect;
      DoDragDrop(pdto, pds, DROPEFFECT_COPY | DROPEFFECT_LINK,
                 &dwEffect);
      pds->Release();
    }
    pdto->Release();
  }
}

    HANDLE_MSG(hwnd, WM_LBUTTONDOWN, OnLButtonDown);

To drag an object, you need two things, a data object and a drop source. We created our drop source above, and the data object comes from the shell. All that’s left to do is start the drag/drop operation by calling the DoDragDrop function.

Notice that we specify that the permitted operations are DROPEFFECT_COPY and DROPEFFECT_LINK. We specifically disallow DROPEFFECT_MOVE because this program doesn’t present a folder-like window; the user has no expectation that the drag/drop will result in a Move operation.

Next time, adding Move support, just to see how it works.

Comments (20)
  1. Mat Hall says:

    "C:\Windows\clock.avi"

    Hard-coded paths? For shame!

  2. Uwe Keim says:

    That is an example, Mat :-)

  3. Mat Hall says:

    I know it’s an example, but it’s "just examples" like that that lead developers into doing things that are liable to break, thus requiring masses of appcompat kludges and shims that bloat the OS, which in turn leads to a lot of whining and complaining in the comments on this blog… Raymond’s just making a rod for his own back. :)

  4. Fat Agnus and Denise says:

    Ok, Matt – how would YOU write this example? A call to GetOpenFileName, perhaps? Or .ini file containing the path?

  5. Fat Agnus and Denise says:

    Sorry Mat Hall, misspelled your first name :(

  6. Raymond Chen says:

    I used a hard-coded path because 1. it allows cut-paste-run, and 2. writing the whole GetOpenFileName thing would have distracted from the point of the article.

  7. Ben Cooke says:

    I read Mat’s "complaint" as being more about the hardcoded path to the Windows directory. That won’t work if Windows is in c:winnt or f:windows.

    Not to mention that I don’t have that stupid video file on my system anymore! What’s that used for, anyway?

  8. Cooney says:

    Not to mention that I don’t have that stupid video file on my system anymore! What’s that used for, anyway?

    It’s a dandy example file for demonstrations.

  9. Matt Green says:

    Thanks for the intro article, and what is with the dogmatic trolling? He writes this in his spare time.

    And I’m sure the MS devs stay up late at night worrying about all the hardcoded references to clock.avi. :)

  10. Mat Hall says:

    "I read Mat’s "complaint" as being more about the hardcoded path to the Windows directory. That won’t work if Windows is in c:winnt or f:windows"

    That was precisely my point — Raymond is continually giving us little insights in to why some parts of Windows behave in unexpected ways, and normally the explanation is "because some developer hardcoded [something] on the assumption it won’t ever change, so we had to add scads of code to stop it breaking in the future".

    "what is with the dogmatic trolling?"

    It certainly wasn’t trolling, and wasn’t even intended as a criticism per se; I appreciate what Raymond does, and wouldn’t dream of complaining! It was merely an observation that although it’s a little throwaway sample program it’s falling in to the trap that has caused so many problems in the past. My experience tells me that a lot of production code out there started life as a "test project", and rather than start again things just get piled on top and before you know it it’s out the door. Better to do it the "right" way from the start!

  11. Raymond Chen says:

    I assumed that it was so obviously a hardcoded path for expository purposes that it didn’t require special remarks. I mean, who would write a real program that operates only on c:windowsclock.avi?

  12. Jerry Pisk says:

    "I mean, who would write a real program that operates only on c:windowsclock.avi?"

    The same whose programs don’t work if you install them into Program Files, because they can’t handle a space in a path. Same people who write inside of the windows directory during normal operation. Same people who close those WFP dialogs during an installation for you. You woudl not believe how many "programmers" use sample code without any modification and the hoops they go through to get around the issues they cause by doing that. I can easily see somebody replacing C:Windowsclock.avi with their file just to get their code working.

  13. Jay Lauffer says:

    I’m more interested in the details on the OleInitialize.. I’m sure there are huge books at Barnes & Nobels that I could read for free while sitting on their comfy chairs, but I just have to ask about the whole Ole single threaded issue? Heck I can’t even formulate the question… why is it that apps initialized with OleInitialize need to be single threaded? I mean I think I know how the DoDragDrop loop works, but what else is going on with OleInitialize that makes it so different from CoInitialize?? Also I’ve always used GiveFeedback with the DRAGDROP_S_USEDEFAULTCURSORS flag, but what if I didn’t what are my options? That’s one I’ve been wondering for awhile..

    To comment or not to comment on the hardcoded path?

    "With half a smile and half a spurn,

    As housewifes do a fly."

    -Emily Dickinson

  14. Mike Dimmick says:

    Jerry: I call it, "the road to hell is paved with good intentions, one step at a time". Each small step is a hack, but the programmer is too tied in with the end goal to stop after a couple of hacks and work out a better approach.

    This sort of programmer sees an obstacle, and hacks out a way around it. They don’t stop to consider whether they should work round the issue; they have no consideration that there could be a good reason why the obstacle exists. Security? Reliability? Reproducibility? Pfah!

  15. Ian Nowland says:

    Whilst Mat is correct in saying that there are always going to be programmers who take example code and run the wrong way with it, I believe his criticism here is off base. Raymond could make all his sample code bulletproof, but this needlessly complicates the "meat" of the sample with a bunch of extraneous detail, which "good" programmers will have to sift through to understand what is being demonstrated. It also wastes Raymond’s time.

    Hardcoded string values are spotted in 5 seconds in code reviews. If you work for someone who has so little respect for software engineering that they don’t have code reviews then no amount of effort by Raymond in writing samples you start from is ever going to make your code good.

  16. AC says:

    Re Jay:

    Apps using OleInitialize don’t need to single threaded – it just means that using an object in multiple threads (apartments) will require call marshaling, which is an expensive process to ensure that the object behaves as if it were only accessed from one thread.

    OleInitialize initiaizes several OLE subsystem, including the OLE clipboard, OLE drag-n-drop and the OLE shared menu. OleInitialize calls CoInitializeEx to do the dirty work of creating a single-threaded apartment for you.

    The options for CDropSource::GiveFeedback are to either return the special value as Raymond does, or to call SetCursor with whatever cursor you want.

  17. Jay Lauffer says:

    Single-Threaded Apartments..

    I knew I should have posed the question better, why does OLE require a single threaded apartment? I mean I can understand that with drag and drop you wouldn’t want the some other thread to alter the IDataObject implementor, I suppose that the clipboard might be the same way (it appears I’m answering my own question), but I’ve always thought there was more to the OLE single threaded-ness (like something down in the guts which could only be supported in a single threaded apartment)?

    Basically to implement drag and drop you have to place your main app thread into a single threaded apartment, then any other threads that you might spawn you can initialize into a multithreaded apartment (at your own risk), but this leaves you with that awful marshalling penalty. It seems preferable to use a semaphore to protect your data object while in the drag and drop loop and maintain the advantages of the multithreaded apartment. What am I missing (aside from all the moans and groans about the cost of synchronizing an object, and all that concern over putting the responsibility to protect the object in the lap of the implementor)? A race condition?

    I just can’t get past this suspicion that somewhere in the guts of OLE is a tie-in to something so hideously grotesque that we developers are shielded from by the virtuous single threaded apartment.

  18. Moi says:

    "This is the first of a five-part series"

    Would someone nudge me awake in a week or so, please :-)

  19. Euro says:

    why does OLE require a single threaded apartment?

    I think that there are two reasons: First, when OLE was developed (OLE 2.0 — the COM-based version we still live with — came out in the Windows 3.1 era), there was no such thing as multiple threads, let alone multi-threaded apartments. But, perhaps MS could reimplement the code to be thread-neutral if it didn’t break any compatibility and if it made sense. However I believe it doesn’t make sense, because of the second reason:

    OLE’s purpose (remember, "Object Linking and Embedding") is to allow one application to display and manipulate visual objects inside another application’s windows, and these GDI objects have thread affinity (can only be manipulated from the thread that created them). So, all the code that runs inside the OLE environment must run in a single threaded anyway. (Disclaimer; I’ve done a lot of COM, but little or no OLE so I might be off-base here)

  20. Just wanted to point out a *minor* syntax error in the code:

    In the line "if (!(grfKeyState & (MK_LBUTTON | MK_RBUTTON))" in CDropSource::QueryContinueDrag there should be one more ")" in the end.

    Thanks for the articles Raymond, I’m looking forward to the rest of the series :)

Comments are closed.