What a drag: Dragging text


This week’s mini-series was almost titled “It’s the least you could do” because I’m going to try to do the absolute least amount of work to accomplish something interesting with drag and drop. The real purpose today is to lay some groundwork, but just to have something to show for our effort, I’ll show you how to drag text around.

We’re going to need the CDropSource class from an earlier series on drag-and-drop. Also take the change from CoInitialize to OleInitialize (and similarly CoUninitialize), as well as the line

    HANDLE_MSG(hwnd, WM_LBUTTONDOWN, OnLButtonDown);

Our mission for today is to create the tiniest data object possible.

#include <strsafe.h> // for StringCchCopy
#include <shlobj.h> // (will be needed in future articles)

/* note: apartment-threaded object */
class CTinyDataObject : public IDataObject
{
public:
  // IUnknown
  STDMETHODIMP QueryInterface(REFIID riid, void **ppvObj);
  STDMETHODIMP_(ULONG) AddRef();
  STDMETHODIMP_(ULONG) Release();

  // IDataObject
  STDMETHODIMP GetData(FORMATETC *pfe, STGMEDIUM *pmed);
  STDMETHODIMP GetDataHere(FORMATETC *pfe, STGMEDIUM *pmed);
  STDMETHODIMP QueryGetData(FORMATETC *pfe);
  STDMETHODIMP GetCanonicalFormatEtc(FORMATETC *pfeIn,
                                     FORMATETC *pfeOut);
  STDMETHODIMP SetData(FORMATETC *pfe, STGMEDIUM *pmed,
                       BOOL fRelease);
  STDMETHODIMP EnumFormatEtc(DWORD dwDirection,
                             LPENUMFORMATETC *ppefe);
  STDMETHODIMP DAdvise(FORMATETC *pfe, DWORD grfAdv,
                    IAdviseSink *pAdvSink, DWORD *pdwConnection);
  STDMETHODIMP DUnadvise(DWORD dwConnection);
  STDMETHODIMP EnumDAdvise(LPENUMSTATDATA *ppefe);

  CTinyDataObject();

private:
  enum {
    DATA_TEXT,
    DATA_NUM,
    DATA_INVALID = -1,
  };

  int GetDataIndex(const FORMATETC *pfe);

private:
  ULONG m_cRef;
  FORMATETC m_rgfe[DATA_NUM];
};

We’ll learn more about those private members later. Let’s start with the boring stuff: The IUnknown interface.

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

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

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

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

Yawners. The constructor is interesting, though, because we use our constructor to build the array of supported FORMATETCs which other members will consult.

void SetFORMATETC(FORMATETC *pfe, UINT cf,
                  TYMED tymed = TYMED_HGLOBAL, LONG lindex = -1,
                  DWORD dwAspect = DVASPECT_CONTENT,
                  DVTARGETDEVICE *ptd = NULL)
{
  pfe->cfFormat = (CLIPFORMAT)cf;
  pfe->tymed    = tymed;
  pfe->lindex   = lindex;
  pfe->dwAspect = dwAspect;
  pfe->ptd      = ptd;
}

CTinyDataObject::CTinyDataObject() : m_cRef(1)
{
  SetFORMATETC(&m_rgfe[DATA_TEXT], CF_TEXT);
}

Our data object contains only thing: plain text. We set the clipboard format to CF_TEXT, indicating that that’s the data we have. The type medium is TYMED_HGLOBAL because we are going to provide the text in the form of an HGLOBAL. The other fields are boilerplate that you will rarely have to change: The aspect is DVASPECT_CONTENT because we are going to provide the actual data content. The DVTARGETDEVICE is NULL because our content is device-independent. And the lindex is -1 because we’re going to provide all the data. I’ve created a helper function which uses the boilerplate values as default parameters.

The first member function that will use this helper array is one that we will use quite a bit to do the preliminary validation of incoming FORMATETC structures.

int CTinyDataObject::GetDataIndex(const FORMATETC *pfe)
{
  for (int i = 0; i < ARRAYSIZE(m_rgfe); i++) {
    if (pfe->cfFormat == m_rgfe[i].cfFormat &&
       (pfe->tymed    &  m_rgfe[i].tymed)   &&
        pfe->dwAspect == m_rgfe[i].dwAspect &&
        pfe->lindex   == m_rgfe[i].lindex) {
      return i;
    }
  }
  return DATA_INVALID;
}

The GetDataIndex method takes a candidate FORMATETC and looks to see whether it matches any of the ones in our table of supported formats, m_rgfe, returning its index or DATA_INVALID indicating that there was no match. Note that we consider it a match if any of the requested type media match the supported type media. For example, the caller might pass TYMED_HGLOBAL | TYMED_STREAM, indicating that the caller can handle receiving either an HGLOBAL or an IStream. If our format matches either one, then we’ll call that a success.

Before we continue, here’s a handy helper function when working with clipboard data: It takes a block of memory and turns it into a HGLOBAL.

HRESULT CreateHGlobalFromBlob(const void *pvData, SIZE_T cbData,
                              UINT uFlags, HGLOBAL *phglob)
{
  HGLOBAL hglob = GlobalAlloc(uFlags, cbData);
  if (hglob) {
    void *pvAlloc = GlobalLock(hglob);
    if (pvAlloc) {
      CopyMemory(pvAlloc, pvData, cbData);
      GlobalUnlock(hglob);
    } else {
      GlobalFree(hglob);
      hglob = NULL;
    }
  }
  *phglob = hglob;
  return hglob ? S_OK : E_OUTOFMEMORY;
}

The money in a data object lies in the IDataObject::GetData method, because this is where the data object client gets to see what all the excitement is about.

CHAR c_szURL[] = "http://www.microsoft.com/";

HRESULT CTinyDataObject::GetData(FORMATETC *pfe, STGMEDIUM *pmed)
{
  ZeroMemory(pmed, sizeof(*pmed));

  switch (GetDataIndex(pfe)) {
  case DATA_TEXT:
    pmed->tymed = TYMED_HGLOBAL;
    return CreateHGlobalFromBlob(c_szURL, sizeof(c_szURL),
                              GMEM_MOVEABLE, &pmed->hGlobal);
  }

  return DV_E_FORMATETC;
}

Wow, that was deceptively simple. We ask GetDataIndex to look up the FORMATETC; if it’s DATA_TEXT, we return the desired text in the form of an HGLOBAL. Otherwise, it’s not supported, so we return an appropriate error code. Note that CF_TEXT is specifically ANSI text. For Unicode text, use CF_UNICODE.

Very closely related to IDataObject::GetData is IDataObject::QueryGetData, which is just like GetData except that it doesn’t actually get the data. It just says whether the data object contains data in the specified format.

HRESULT CTinyDataObject::QueryGetData(FORMATETC *pfe)
{
  return GetDataIndex(pfe) == DATA_INVALID ? S_FALSE : S_OK;
}

The only other interesting method is IDataObject::EnumFormatEtc, which can be asked to return an enumerator that lists all the formats contained in the data object.

HRESULT CTinyDataObject::EnumFormatEtc(DWORD dwDirection,
                                       LPENUMFORMATETC *ppefe)
{
  if (dwDirection == DATADIR_GET) {
    return SHCreateStdEnumFmtEtc(ARRAYSIZE(m_rgfe), m_rgfe, ppefe);
  }
  *ppefe = NULL;
  return E_NOTIMPL;
}

If the caller is asking for the formats that it can “get”, then we return an enumerator created from the shell stock format enumerator. Otherwise, we say that we don’t have one.

The rest of the methods are just stubs.

HRESULT CTinyDataObject::GetDataHere(FORMATETC *pfe,
                                     STGMEDIUM *pmed)
{
    return E_NOTIMPL;
}

HRESULT CTinyDataObject::GetCanonicalFormatEtc(FORMATETC *pfeIn,
                                               FORMATETC *pfeOut)
{
  *pfeOut = *pfeIn;
  pfeOut->ptd = NULL;
  return DATA_S_SAMEFORMATETC;
}

HRESULT CTinyDataObject::SetData(FORMATETC *pfe, STGMEDIUM *pmed,
                                                   BOOL fRelease)
{
    return E_NOTIMPL;
}

HRESULT CTinyDataObject::DAdvise(FORMATETC *pfe, DWORD grfAdv,
                     IAdviseSink *pAdvSink, DWORD *pdwConnection)
{
    return OLE_E_ADVISENOTSUPPORTED;
}

HRESULT CTinyDataObject::DUnadvise(DWORD dwConnection)
{
    return OLE_E_ADVISENOTSUPPORTED;
}

HRESULT CTinyDataObject::EnumDAdvise(LPENUMSTATDATA *ppefe)
{
    return OLE_E_ADVISENOTSUPPORTED;
}

And we’re done. Let’s take it for a spin.

void OnLButtonDown(HWND hwnd, BOOL fDoubleClick,
                   int x, int y, UINT keyFlags)
{
  IDataObject *pdto = new CTinyDataObject();
  if (pdto) {
    IDropSource *pds = new CDropSource();
    if (pds) {
      DWORD dwEffect;
      DoDragDrop(pdto, pds, DROPEFFECT_COPY, &dwEffect);
      pds->Release();
    }
    pdto->Release();
  }
}

Fire up Wordpad and then click in the client area of our scratch program and drag and drop the invisible text over to the Wordpad window. Ta-da, the text is inserted.

This even works with Firefox to drag a URL into a Firefox window. But it doesn’t work for Internet Explorer. We’ll see why next time.

Exercise: Why didn’t we also have to set CF_UNICODE text?

Pre-emptive Igor Levicki comment: “Windows Vista should be dragged and dropped to the trash can.”

Comments (19)
  1. Tim Smith says:

    To the best of my knowledge, conversions between CF_UNICODE and CF_TEXT (both directions) are handled automatically by the OS.

    The OS can "lie" and insert new FORMATETC structures that are supplied to the consumer.

  2. Matt Green says:

    Hahaha, your Igor comment today is awesome!

  3. Andrew says:

    Nitpick:

    If CTinyDataObject::QueryInterface fails then the value of the ppv param should be left unchanged.

  4. Mike Dimmick says:

    @Andrew: I’m sure Raymond’s told us in previous posts that many callers of QueryInterface don’t check the return value and base their worked/not-worked decision on the returned pointer. Safer to set it to NULL and crash callers who are trying to call through a bad pointer straight away.

    Which one of us has been responsible for maintaining the shell and working round bugs in shell extensions?

  5. James Risto says:

    I find that the pre-emptive stuff totally deflates the waiting for any off-topic comments and I can concentrate better on the post!

  6. Triangle says:

    "Safer to set it to NULL and crash callers who are trying to call through a bad pointer straight away."

    Are you sure you really want that?

    "I just installed this program and now whenever I turn on the computer there is some kind of error and then everything else on my screen dissapears"

    "I find that the pre-emptive stuff totally deflates the waiting for any off-topic comments and I can concentrate better on the post!"

    You realize that by stating that you’ve made an offtopic comment, right? :P

  7. Tihiy says:

    Well, drag and drop is broken in Windows Vista for those "security features".

  8. Yuhong Bao says:

    "Well, drag and drop is broken in Windows Vista for those "security features"."

    Well, this actually did happen in one case, you can’t drag and drop to a command prompt window or any other window owned by an elevated process in Vista because of UIPI.

  9. Leo Davidson says:

    @Mike: Some of them don’t check the return value *or* the pointer, which is fun when you’re writing your own ActiveX container!

    A certain document-viewing ActiveX control does a QueryService (QS is like QI but may return a different object) for IBindHost, then calls through *ppv regardless of NULL pointers and HRESULTs. Yay for assuming the current version of Internet Explorer is the only ActiveX container in the world and that even IE itself won’t drop any interfaces one day…

  10. RyanBemrose says:

    @Andrew:

    http://msdn2.microsoft.com/en-us/library/ms682521(vs.85).aspx

    ppvObject

       [out] Address of pointer variable that receives the interface pointer requested in riid. Upon successful return, *ppvObject contains the requested interface pointer to the object. If the object does not support the interface specified in iid, *ppvObject is set to NULL.

  11. BryanK says:

    Triangle: What else are you supposed to do?  You’ll either hand them NULL, or some other random pointer that they’re going to call through (i.e. whatever was being pointed to by the pointer that they gave you).  Better that you hand back NULL (and crash them right away) than let them call off into never-never land through some random pointer that you gave back to them (and let the program either continue "forever" (well, until a stack overflow), or crash a long way from the problem).

    If you don’t support the interface they’re asking for, it’s not like you can hand them your IUnknown or something crazy like that.  You have to either give them NULL, or not change the value.

    (And if the documentation says to give them NULL, then you better give them NULL as well; otherwise people that *do* follow the documentation can get screwed up by your QI implementation.)

  12. Maurits says:

    A more subtle nitpick attempt is whether QueryInterface should check ppv for NULL and return E_POINTER.

  13. Dave says:

    Maurits, any developer dumb enough to pass a NULL into QI is also not going to check its return value. The most likely time for this to happen is during development, and the debugger’s stack trace should make it clear to the developer what they did wrong. The crash accelerates the debugging process.

  14. mvadu says:

    @Dave

    The crash accelerates the debugging process.

    Nicely said.. I like that as a tester. If I could see a crash/Runtime error in the beginning it self my life is easier as I can stop my testing then and there and return the code to dev team.. haha

  15. mvadu says:

    @Dave

    The crash accelerates the debugging process.

    Nicely said.. I like that as a tester. If I could see a crash/Runtime error in the beginning it self my life is easier as I can stop my testing then and there and return the code to dev team.. haha

  16. hege says:

    Raymond, grow up, you’re childish.

    (I don’t care about removing my comment, it’s just a message for you)

  17. GreenReaper says:

    Tihiy, Yuhong Bao: If you’re the program, it can be done, at least for WM_DROPFILES. Just bear in mind that you may need to allow through message types that don’t officially exist.

  18. kokomo says:

    "Windows Vista should be dragged and dropped to the trash can."

    Greatest preemptive comment of all time. I think it’s better than "*Here’s your stupid asterisk."

    Thank you Raymond.

  19. .dan.g. says:

    Shouldn’t QueryGetData() be strictly implemented as:

    return GetDataIndex(pfe) == DATA_TEXT ? S_OK : S_FALSE;

    because DATA_NUM is not handled in GetData()?

Comments are closed.