Shell Style Drag and Drop object disposal using gdi32.dll DeleteObject

2

So... I have recently been developing a Winforms C# application in .NET 2.0 that uses the Shell Style drag and drop described in the great tutorial here: http://blogs.msdn.com/b/adamroot/archive/2008/02/19/shell-style-drag-and-drop-in-net-wpf-and-winforms.aspx

This was used to give a semi-transparent image of the dragged control during the drag action. However, a secondary functionality is that files can be dropped onto this panel, and some action carried out based on the file(s).

A custom control has to be able to be dragged from one flow layout panel and dropped into another (this all already works, no problems here). My problem occurs when I drag the control in the first panel, and drop it into the same panel (i.e. cancelling the drop action). If a file is then dragged onto the panel, the image of the file will be replaced by the image of the former control, the one that was dragged but not dropped (If that makes any sense?)

I believed this to be caused by the Image not being disposed of properly, and it appears I was right. I am attempting to use the gdi32.dll DeleteObject method to dispose of the HBitmap created, but this method always returns false.

The code below shows how the HBitmap object is being set, and is being used to create the drag effect. If the DisposeImage method is called before the call to InitializeFromBitmap, the image will be disposed as expected. But if it is called after, the call will return false, and the image will remain in memory. It seems to be that something is holding onto the HBitmap and won't let go.

private ShDragImage m_DragImageInfo = new ShDragImage();
    public void SetDragImage( System.Runtime.InteropServices.ComTypes.IDataObject theDataObject, Control theControl, System.Drawing.Point theCursorPosition )
    {
        theCursorPosition = theControl.PointToClient( theCursorPosition );
        int Width = theControl.Width;
        int Height = theControl.Height;

        // Ensure that the bitmap is disposed to prevent a memory leak
        IntPtr HBitmap = IntPtr.Zero;
        using ( Bitmap ControlImage = new Bitmap( Width, Height ) )
        {
            theControl.DrawToBitmap( ControlImage, new Rectangle( 0, 0, Width, Height ) );
            HBitmap = ControlImage.GetHbitmap();
            SetDragImage( theDataObject, HBitmap, theCursorPosition, ControlImage.Size );
        }
    }

    private void SetDragImage( System.Runtime.InteropServices.ComTypes.IDataObject theDataObject, IntPtr theHImage, System.Drawing.Point theCursorPosition, Size theImageSize )
    {
        m_DragImageInfo = new ShDragImage();

        Win32Size ImageSize;
        ImageSize.m_Width = theImageSize.Width;
        ImageSize.m_Height = theImageSize.Height;
        m_DragImageInfo.m_DragImageSize = ImageSize;

        Win32Point CursorPosition;
        CursorPosition.m_X = theCursorPosition.X;
        CursorPosition.m_Y = theCursorPosition.Y;

        m_DragImageInfo.m_CursorOffset = CursorPosition;
        m_DragImageInfo.m_ColorKey = Color.Magenta.ToArgb();
        m_DragImageInfo.m_DragImageHBitmap = theHImage;

        try
        {
            IDragSourceHelper sourceHelper = (IDragSourceHelper)new CDragDropHelper();
            sourceHelper.InitializeFromBitmap( ref m_DragImageInfo, theDataObject );
        }
        catch ( NotImplementedException theException )
        {
            DisposeImage();
            throw theException;
        }
    }

    public void DisposeImage()
    {
        CExternalFunctions.DeleteObject( m_DragImageInfo.m_DragImageHBitmap );
    }

Also, here is the class that is being used to store the data. As far as I can see, on disposal the data is being released in the ClearStorage method.

[ComVisible( true )]
public class CDataObject : System.Runtime.InteropServices.ComTypes.IDataObject, IDisposable
{
    private Dictionary<FORMATETC, STGMEDIUM> m_Storage;
    private EventHandler m_Delegate;

    public CDataObject()
    {
        m_Storage = new Dictionary<FORMATETC, STGMEDIUM>();
    }

    private void ClearStorage()
    {
        foreach ( KeyValuePair<FORMATETC, STGMEDIUM> Pair in m_Storage )
        {
            STGMEDIUM Medium = Pair.Value;
            CExternalFunctions.ReleaseStgMedium( ref Medium );
        }
        m_Storage.Clear();
    }

    public void SetDelegate( EventHandler theDelegate )
    {
        m_Delegate = theDelegate;
    }

    public void Dispose()
    {
        Dispose( true );
    }

    private void Dispose( bool isDisposing )
    {
        if ( isDisposing )
        {
            ClearStorage();
        }
    }

    #region COM IDataObject Members

    #region COM constants

    private const int c_AdviseNotSupported = unchecked( (int)0x80040003 );

    private const int c_FormatEtc = unchecked( (int)0x80040064 );
    private const int c_TypeMismatch = unchecked( (int)0x80040069 );
    private const int c_WrongFormat = unchecked( (int)0x8004006A );

    #endregion // COM constants

    #region Unsupported functions

    public int DAdvise( ref FORMATETC pFormatetc, ADVF advf, IAdviseSink adviseSink, out int connection )
    {
        throw Marshal.GetExceptionForHR( c_AdviseNotSupported );
    }

    public void DUnadvise( int connection )
    {
        throw Marshal.GetExceptionForHR( c_AdviseNotSupported );
    }

    public int EnumDAdvise( out IEnumSTATDATA enumAdvise )
    {
        throw Marshal.GetExceptionForHR( c_AdviseNotSupported );
    }

    public int GetCanonicalFormatEtc( ref FORMATETC formatIn, out FORMATETC formatOut )
    {
        formatOut = formatIn;
        return c_FormatEtc;
    }

    public void GetDataHere( ref FORMATETC format, ref STGMEDIUM medium )
    {
        throw new NotSupportedException();
    }

    #endregion // Unsupported functions

    public IEnumFORMATETC EnumFormatEtc( DATADIR theDirection )
    {
        EnumFORMATETC EnumFormat = null;
        // We only support GET
        if ( theDirection == DATADIR.DATADIR_GET )
        {
            EnumFormat = new EnumFORMATETC( m_Storage );
        }
        else
        {
            throw new NotImplementedException( "EnumFormatEtc method is not implemented" );
        }

        return EnumFormat;
    }

    public void GetData( ref FORMATETC theFormat, out STGMEDIUM theMedium )
    {
        theMedium = new STGMEDIUM();
        foreach ( KeyValuePair<FORMATETC, STGMEDIUM> Pair in m_Storage )
        {
            if ( ( Pair.Key.tymed & theFormat.tymed ) > 0
                && Pair.Key.dwAspect == theFormat.dwAspect
                && Pair.Key.cfFormat == theFormat.cfFormat )
            {
                STGMEDIUM Medium = Pair.Value;
                theMedium = CopyMedium( ref Medium );
                break;
            }
        }
    }

    public int QueryGetData( ref FORMATETC format )
    {
        int ReturnValue;

        ReturnValue = c_TypeMismatch;

        // Try to locate the data
        // TODO: The ret, if not S_OK, is only relevant to the last item
        foreach ( FORMATETC FormatEtc in m_Storage.Keys )
        {
            if ( ( FormatEtc.tymed & format.tymed ) > 0 )
            {
                if ( FormatEtc.cfFormat == format.cfFormat )
                {
                    // Found it, return S_OK;
                    ReturnValue = 0;
                    break;
                }
                else
                {
                    // Found the medium type, but wrong format
                    ReturnValue = c_WrongFormat;
                }
            }
            else
            {
                // Mismatch on medium type
                ReturnValue = c_TypeMismatch;
            }
        }

        return ReturnValue;
    }

    public void SetData( ref FORMATETC theFormatIn, ref STGMEDIUM theMedium, bool theRelease )
    {
        // If the format exists in our storage, remove it prior to resetting it
        foreach ( FORMATETC FormatEtc in m_Storage.Keys )
        {
            if ( ( FormatEtc.tymed & theFormatIn.tymed ) > 0
                && FormatEtc.dwAspect == theFormatIn.dwAspect
                && FormatEtc.cfFormat == theFormatIn.cfFormat )
            {
                m_Storage.Remove( FormatEtc );
                break;
            }
        }

        // If release is true, we'll take ownership of the medium.
        // If not, we'll make a copy of it.
        STGMEDIUM Medium = theMedium;
        if ( !theRelease )
        {
            Medium = CopyMedium( ref theMedium );
        }

        m_Delegate( this, new EventArgs() );

        m_Storage.Add( theFormatIn, Medium );
    }

    public void SetDataEx( string theFormat, object theData )
    {
        DataFormats.Format DataFormat = DataFormats.GetFormat( theFormat );

        // Initialize the format structure
        FORMATETC FormatETC = new FORMATETC();
        FormatETC.cfFormat = (short)DataFormat.Id;
        FormatETC.dwAspect = DVASPECT.DVASPECT_CONTENT;
        FormatETC.lindex = -1;
        FormatETC.ptd = IntPtr.Zero;

        // Try to discover the TYMED from the format and data
        TYMED Tymed = TYMED.TYMED_HGLOBAL;
        // If a TYMED was found, we can use the system DataObject
        // to convert our value for us.
        FormatETC.tymed = Tymed;

        // Set data on an empty DataObject instance
        DataObject DataObject = new DataObject();
        DataObject.SetData( theFormat, true, theData );

        // Now retrieve the data, using the COM interface.
        // This will perform a managed to unmanaged conversion for us.
        STGMEDIUM Medium;
        ( (System.Runtime.InteropServices.ComTypes.IDataObject)DataObject ).GetData( ref FormatETC, out Medium );
        try
        {
            // Now set the data on our data object
            SetData( ref FormatETC, ref Medium, true );
        }
        catch( Exception theException )
        {
            // Ensure the Medium is released if there are any problems
            CExternalFunctions.ReleaseStgMedium( ref Medium );
            throw theException;
        }
    }

    private STGMEDIUM CopyMedium( ref STGMEDIUM theMedium )
    {
        STGMEDIUM Medium = new STGMEDIUM();
        int Return = CExternalFunctions.CopyStgMedium( ref theMedium, ref Medium );
        if ( Return != 0 )
        {
            // If the copy operation fails, throw an exception using the HRESULT
            throw Marshal.GetExceptionForHR( Return );
        }

        return Medium;
    }

    #endregion

    [ComVisible( true )]
    private class EnumFORMATETC : IEnumFORMATETC
    {
        // Keep an array of the formats for enumeration
        private FORMATETC[] m_Formats;
        // The index of the next item
        private int m_CurrentIndex = 0;

        private const int c_OK = 0;
        private const int c_Failed = 1;

        internal EnumFORMATETC( Dictionary<FORMATETC, STGMEDIUM> storage )
        {
            // Get the formats from the list
            m_Formats = new FORMATETC[ storage.Count ];
            int Index = 0;
            foreach ( FORMATETC FormatEtc in storage.Keys )
            {
                m_Formats[ Index ] = FormatEtc;
                Index++;
            }
        }

        private EnumFORMATETC( FORMATETC[] theFormats )
        {
            // Get the formats as a copy of the array
            m_Formats = new FORMATETC[ theFormats.Length ];
            theFormats.CopyTo( this.m_Formats, 0 );
        }

        #region IEnumFORMATETC Members

        public void Clone( out IEnumFORMATETC theEnum )
        {
            EnumFORMATETC ReturnEnum = new EnumFORMATETC( m_Formats );
            ReturnEnum.m_CurrentIndex = m_CurrentIndex;
            theEnum = ReturnEnum;
        }

        public int Next( int theNumberOfElements, FORMATETC[] theRequestedFormats, int[] theNumberOfRequests )
        {
            // Start with zero fetched, in case we return early
            if ( theNumberOfRequests != null && theNumberOfRequests.Length > 0 )
            {
                theNumberOfRequests[ 0 ] = 0;
            }

            // This will count down as we fetch elements
            int ReturnCount = theNumberOfElements;

            int ReturnValue = c_OK;

            // Short circuit if they didn't request any elements, or didn't
            // provide room in the return array, or there are not more elements
            // to enumerate.
            if ( theNumberOfElements <= 0 || theRequestedFormats == null || m_CurrentIndex >= m_Formats.Length )
            {
                ReturnValue = c_Failed;
            }

            // If the number of requested elements is not one, then we must
            // be able to tell the caller how many elements were fetched.
            if ( ( theNumberOfRequests == null || theNumberOfRequests.Length < 1 ) && theNumberOfElements != 1 )
            {
                ReturnValue = c_Failed;
            }

            // If the number of elements in the return array is too small, we
            // throw. This is not a likely scenario, hence the exception.
            if ( theRequestedFormats.Length < theNumberOfElements )
            {
                throw new ArgumentException( "The number of elements in the return array is less than the number of elements requested" );
            }

            // Fetch the elements.
            for ( int i = 0; m_CurrentIndex < m_Formats.Length && ReturnCount > 0; i++ )
            {
                theRequestedFormats[ i ] = m_Formats[ m_CurrentIndex ];
                ReturnCount--;
                m_CurrentIndex++;
            }

            // Return the number of elements fetched
            if ( theNumberOfRequests != null && theNumberOfRequests.Length > 0 )
            {
                theNumberOfRequests[ 0 ] = theNumberOfElements - ReturnCount;
            }

            if ( ReturnCount != 0 )
            {
                ReturnValue = c_Failed;
            }

            return ReturnValue;
        }

        public int Reset()
        {
            m_CurrentIndex = 0;
            return c_OK;
        }

        /// <summary>
        /// Skips the number of elements requested.
        /// </summary>
        /// <param name="celt">The number of elements to skip.</param>
        /// <returns>If there are not enough remaining elements to skip, returns S_FALSE. Otherwise, S_OK is returned.</returns>
        public int Skip( int theNumberOfElementsToSkip )
        {
            int Success = c_OK;
            if ( m_CurrentIndex + theNumberOfElementsToSkip > m_Formats.Length )
            {
                Success = c_Failed;
            }

            m_CurrentIndex += theNumberOfElementsToSkip;
            return Success;
        }

        #endregion
    }

Is there something I've missed? Is there a way to force this HBitmap to be released so that I can dispose of it properly? Any help on this would be greatly appreciated.

Edit: Really would be great to get any sort of assistance in this problem. I've tried ensuring that the HDC is released, and this still resulted in the call to DeleteObject returning false.

c#
drag-and-drop
gdi
windows-shell
dispose
asked on Stack Overflow Feb 7, 2012 by LostToApathy • edited Aug 10, 2012 by Henk Langeveld

0 Answers

Nobody has answered this question yet.


User contributions licensed under CC BY-SA 3.0