Home | Chi sono | Contattami
 

Progr. lineare

Delphi
 
Componenti
  Database
 
Miei articoli

Windows

Miei articoli 

 

Drag 'n Drop


In questo articolo andremo a parlare un pò di Drag 'n Drop ossia la possibilità di trascinare elementi (ad esempio trascinare un oggetto nel Cestino causandone la cancellazione è un esempio di Drag 'n Drop). Tempo addietro e per essere più precisi nel 1998 era attivo un bel portale inerente la programmazione Delphi all'url http://www.undu.com; UNDU stà per Unofficial Neswletter of Delphi Users e gli appassionati di Delphi si divertivano a postare i loro Tips o anche dei veri e propri articoli. Spesso si trovavano articoli molti tecnici e un bel giorno Grahame Marsh ha postato un articolino sul Drag 'n Drop: tutti sappiamo che i controlli visuali di Delphi (tipo ad esempio la TListBox) prevedono l'evento OnDragDrop tuttavia quest'evento riguarda le operazioni di Drag 'n Drop che hanno come Drag Source (ossia come oggetto che viene trascinato) un oggetto della medesima applicazione. In questo articolo veniva affrontato il problema del Drag 'n Drop dall'esterno. Visto il successo e l'interesse suscitato, a questo articolo ne sono seguiti circa altri 10 fino al 2000 quando lo stesso autore ha pubblicato un articolo in Word comprensivo di tutte le considerazioni fatte negli ultimi 2 anni. In allegato una libreria di componenti che implementavano le tecnologie analizzate con un bel numero di esempi. A quei tempi era appena uscito Delphi 5 (ed aveva anche visto al luce Windows 2000); in questi giorni, spulciando nei salvataggi sui miei hard disk ho ritrovato questo materiale ed ho deciso di andarlo a rivisitare visto che ai tempi mi aveva catturato parecchio. La prima cosa è stata l'installazione dei componenti: mi sono creato un nuovo Package e devo dire che il passaggio da Delphi 5 a Delphi 7 non è stato poi così doloroso; all'interno della unit OleRe.pas ho sostituito il termine Null (valore Variant nullo per Delphi 5) con varNull (valore Variant nullo valido per Delphi 7); poi nella unit OleRegister.pas (che include le procedure di registrazione dei componenti ma anche l'implementazione dei vari Property Editor e Component Editor) ho sostituito nella clausola Uses la unit obsoleta DsgnIntf con le 2 unit DesignIntf e DesignEditors (che si trovano nella cartella $(DELPHI)\Source\ToolsApi che va appunto aggiunta all'Environment Library Path di Delphi). Poi nella sezione Requires del mio Package ho aggiunto designide.dcp (che si trova nella cartella $(DELPHI)\Lib). Inoltre nel pacchetto originale era presente la cartella relativa al componente per la compressione JPeg che, ai tempi di Delphi 5, era un componente aggiuntivo presente nel CD di installazione di Delphi, ma che in Delphi 7 è già incluso di default nell'installazione. Bene... compilato ed installato con successo: Eurekaaa!!!! I componenti si installano nei seguenti 3 Tab della Paletta dei componenti di Delphi: MyControls, OLE, OLE UI (che chiaramente possono essere cambiati modificando la unit OleRegister.pas che contiene appunto le procedure di registrazione). Ho cominciato quindi a provare tutti gli esempi; l'unico problema l'ho riscontrato con gli esempi nelle cartelle Chapter8a e Chapter9b che trattano la copia nella clipboard di formati grafici quali Icone (.ico), Bitmaps (.bmp), Jpeg (.jpg o .jpeg), Windows Metafile (.wmf), Enhanced Metafile (.emf) e Gif (.gif): in pratica i file grafici in questione vengono gestiti usando il componente TImage ed inglobati direttamente nel file .dfm della form tramite la property Picture di TImage; alcuni dei file erano un pò grandini ed impallavano Delphi, cos' li ho sostituiti con dei miei file più leggeri (ed anche più carini, ehehe) e tutto è andato OK. A questo punto sono andato alla ricerca di dove potesse essere tuttora pubblicato l'articolo orginale visto che UNDU non è più tra noi da almeno 5 anni e ahimè non ho trovato nulla; allora ho deciso di prendere il file Word, riformattarlo a dovere e pubblicare il documento originale. Quindi di seguito l'articolo originale sul Drag 'n Drop.
   

COM Interface based Drag and Drop
Originally published in UNDU from about mid 1998 to early 2000.

Grahame Marsh
gsmarsh@aol.com


Foreword

When the original series in UNDU came to an end, a number of readers asked me to wrap the series into a single article. This document is the response to that request in that it is a consolidated version of a series of articles. The demonstrations have been reorganised (more or less) to use a single library of components and the text has been revised into a series of chapters. This is not a particularly well polished document, and I hope you will forgive me for not spending an awful lot of time preparing it. There some new information in this text, and the drag and drop component library has been enhanced since the last article (together with the removal of a yet a few more bugs).

A few points to note:

• The demonstrations have only been test (recently) with Delphi 5
• The components evolved over the course of the series. The demonstrations (other than DTView) and components here do not "evolve" but use the finalised versions.

Introduction

My intention is to provide you with some working code and examples, but not necessarily with well polished components to go and use. This is mainly because I could have entitled the article "A beginners guide to....." where I'm one of the beginners.

Finally, if the "COM Interfaced" part of the title has worried you or put you off then don't be, the extent of the use of Interfaces is quite limited and I think simple to follow as all of the interfaces used are predefined by the Windows drag and drop system. The only nasty bits are the direct translation of C++ variable names into Pascal, here I have tried to isolate them in low level methods or procedures.


Chapter 1 - Adding OLE Drop Target Capabilities

Drag Source and Drop Targets

Your application can be where users drag objects etc from (i.e. a Source), or where they can drop objects into (i.e. a Target) or both. This chapter will be limited to being a Drop Target and I will cover being a Drag Source later. Just being a Drop Target opens up so many possibilities to explore that I will take several chapters to describe and illustrate them all, but I will end each chapter with something concrete.

Main Steps

The two main steps involved in making you application a Drop Target. This first is to provide an IDropTarget interface. The interface is defined in the ActiveX unit as:
 

IDropTarget = interface(IUnknown) ['{00000122-0000-0000-C000-000000000046}'] function DragEnter( const dataObj: IDataObject; grfKeyState: Longint; pt: TPoint; var dwEffect: Longint ): HResult; stdcall; function DragOver( grfKeyState: Longint; pt: TPoint; var dwEffect: Longint ): HResult; stdcall; function DragLeave: HResult; stdcall; function Drop( const dataObj: IDataObject; grfKeyState: Longint; pt: TPoint; var dwEffect: Longint ): HResult; stdcall; end;

It looks horrible but one simple way of looking at this interface is to think that you only need to write four call back procedures for four events that Windows will need to inform you of and ask you how you what them handled. One way to provide the interface is to write a class derived from TInterfacedObject (which is defined in the System unit):

type TDropTarget = class (TInterfacedObject, IDropTarget) function DragEnter( const DataObj: IDataObject; grfKeyState: Longint; pt: TPoint; var dwEffect: Longint ): HResult; stdcall; function DragOver( grfKeyState: Longint; pt: TPoint; var dwEffect: Longint ): HResult; stdcall; function DragLeave: HResult; stdcall; function Drop( const DataObj: IDataObject; grfKeyState: Longint; pt: TPoint; var dwEffect: Longint ): HResult; stdcall; end; . . . . function TDropTarget.DragEnter( const DataObj: IDataObject; grfKeyState: Longint; pt: TPoint; var dwEffect: Longint ): HResult; begin . . . . end; function TDropTarget.DragOver( grfKeyState: Longint; pt: TPoint; var dwEffect: Longint ): HResult; begin . . . . end; function TDropTarget.DragLeave: HResult; begin . . . . end; function TDropTarget.Drop( const DataObj: IDataObject; grfKeyState: Longint; pt: TPoint; var dwEffect: Longint ): HResult; begin . . . . end;

The way these four methods work as follows. When the mouse first enters a window registered as a drop target (I know I’ve not said how you register a window yet – we must first define our interface) Windows calls the DragEnter method. This method receives as parameters the data being transferred as an interface, but not one that we write here, it is provided by the data source; the state of the shift, control, alt and mouse keys and the mouse position. We can return a desired operation (we want to link, copy or move the data) or say that the drop data is not wanted. So the main thing to do in the DragEnter method is to check on the acceptability of the data. But you can also give visual feedback to show how the object will appear if dropped. The method is a function and we can return a code to indicate the success of the call, or some error condition.

When the cursor moves around inside a drop target window, the DragOver event is repeatedly fired. The method receives the shift key status and mouse position as parameters and we can return the desired operation and again a function result. In this method we can update any visual feedback started in the DragEnter method call. Because it is called so often you need to make this method efficient as possible.

Should the cursor move out of the drop window then the DragLeave method is called. This call receives no parameters but you use it to remove any visual feedback you displayed during DragEnter or DragOver.

Finally, the Drop method is called when the user drops the data object in the registered window. This method has the same parameters as the DragEnter method and here you must copy (or link) the appropriate data from the data object. For example, if you were allowing the data to be dropped on a TOleContainer you might do something like this:
 

function TDropTarget.Drop( const DataObject: IDataObject; grfKeyState: Longint; pt: TPoint; var dwEffect: Longint ): HResult; var CreateInfo : TCreateInfo; begin dwEffect := StandardEffect (KeysToShiftState (grfKeyState)); ZeroMemory (@CreateInfo, SizeOf (TCreateInfo)); if dwEffect = deLink then CreateInfo.CreateType := ctLinkFromData else CreateInfo.CreateType := ctFromData; CreateInfo.DataObject := DataObject; OleContainer.CreateObjectFromInfo (CreateInfo) end;

Here I determine if the user wants to Copy, Move or Link; set the appropriate CreateType; and since the TOleContainer is already IDataObject aware, it is a simple matter to pass this to the CreateObjectFromInfo method of the TOleContainer component.

The StandardEffect function returns deMove if no keys are pressed, deCopy if the control key is pressed or deLink if both the control and shift keys are pressed. This is a useful utility function and the redefinition of the DROPEFFECT_XXXX constants and the use of the KeysToShiftState function both help the move from C++ to Delphi.
 

const // Drop effects as Delphi style constants deNone = DROPEFFECT_NONE; deMove = DROPEFFECT_MOVE; deCopy = DROPEFFECT_COPY; deLink = DROPEFFECT_LINK; deScroll = DROPEFFECT_SCROLL; function StandardEffect (Keys : TShiftState) : integer; begin Result := deMove; if ssCtrl in Keys then begin Result := deCopy; if ssShift in Keys then Result := deLink end end;

And Secondly?

So this was the first thing to do, the second is to tell Windows that such and such window is to be used with such and such IDropTarget Interface. This is done using the RegisterDragDrop API function, continuing the example above you might use:
 

RegisterDragDrop (OleContainer.Handle, FDropTarget);

in the form’s OnCreate handler. Where FDropTarget is an instance of the TDropTarget described above. Later on you should call RevokeDragDrop to disconnect the window from the drag and drop mechanics.

An Example

The component you use in this demonstration is a simple helper that entirely provides a drop target capability to a TOleContainer component. Simply place the helper component and TOleComponent on a form; set the helper component properties to link to the OleContainer and set the Active property to true. Run the application and you will find you can drag all manner of things from the Windows desktop or other applications into the OleContainer. The demonstration application contains little more than a form, a OleContainer and this component.

The component has been written slightly differently to the interface above as I have written the IDropTarget interface into the component class directly by using this definition:
 

type TOleDropTarget = class (TComponent, IUnknown, IDropTarget) . . . . end;

This means that we do not have to specifically create the DropSource Interface it will happen automatically and we can pass Self to the RegisterDragDrop call. The Active property is used to call RegisterDragDrop and RevokeDragDrop and each of the four interface methods generate Delphi events for the convenience. But each event is provided with a default handler so you actually do not need to write any code yourself to see the component working. I have done my best to isolate the “C++” stuff from the Delphi goodies by using some translations and renaming Windows defined constants.

I would emphases that this component is intended to demonstrate writing a drag target interface and not a component you will permanently install on you component palette and use and use and use. So after trying it out, delete it again from the palette.

The design form in the IDE:
 

 

And the application with a power point presentation in the container that has be dragged and dropped there:

But, I have left a large number of things started and left undone:

• How do I find out what data is in the DataObject parameter to decide if I can use it?
• How do I get the data out of the DataObject?
• How do I give visual feedback to the user?
• How do I act as a drop source?
 

Chapter 2 – Obtaining Formats

Introduction

In Chapter 1, I showed how the IDropTarget interface is used to enable a TOleContainer to receive dropped OLE objects. This was a bit of a cheat as the IDropTarget interface produces an IDataObject interface which actually contains the data being dropped. The TOleContainer can use an IDataObject directly with only a little code so was easy to implement.

But what if you want to drop text on a TMemo or a metafile on to a TImage? In these cases you must get the data from the IDataObject yourself. So in this part I’ll show the first of these simple cases: Dropping text on a TMemo.


Dropping text onto a TMemo

When the user drags something across a window designated as a drop target the IDropTarget fires its various interface methods as described in Chapter 1. The IDataObject available when the DragEnter and Drop methods are called can contain a wide variety of data formats. So the first task is to determine if some text is available, and then, when the user drops it, the second task is to obtain the text from the interface. This is exactly the same process used by the clipboard where you ask the clipboard if a given format is available and then copy it into the memo control.

To find out what formats are available in the IDataObject it is provided with a system to enable you to enumerate all of the available formats. The enumerator is obtained using the call:
 

var Enum : IEnumFormatEtc; ... FDataObject.EnumFormatEtc(DATADIR_GET, Enum)

The EnumFormatEtc returns a pointer to an IEnumFormatEtc interface. This interface is what we use to find out the formats available. The enumerator interface looks like this:
 

IEnumFORMATETC = interface(IUnknown) ['{00000103-0000-0000-C000-000000000046}'] function Next( celt: Longint; out elt; pceltFetched: PLongint ): HResult; stdcall; function Skip(celt: Longint): HResult; stdcall; function Reset: HResult; stdcall; function Clone(out Enum: IEnumFormatEtc): HResult; stdcall; end;

The methods I’m going to use here are Next which returns the next available format and Reset which sets the enumeration to back to the beginning of the list. Skip can be used to jump over elements in the list and Clone copies the interface.

A call to Next is made like this:

var Returned : integer; FormatEtc : TFormatEtc; ... if Enum.Next (1, FormatEtc, @Returned) = S_OK then begin

In this example, if successful, the next format record is returned in the FormatEtc variable. The Returned variable will contain the value 1 and the function will return the value S_OK. The TFormatEtc record looks like this:

type TFormatEtc = record cfFormat : TClipFormat; ptd : PDVTargetDevice; dwAspect : Longint; lindex : Longint; tymed : Longint; end;

For this discussion, the important fields in this record are cfFormat which contains the (clipboard) format identifier, the one we are looking for is CF_TEXT, and tymed which contains values to identify the way the data is stored. This can take on values such as TYMED_HGLOBAL to show that the data is in a global memory block or TYMED_MFPICT for a metafilepict structure. So it is fairly easy to iterate through the available formats using the interface looking for the format required.

Having found that the required data format is indeed available then we need to get that data from the IDataObject. This is done by using the FormatEtc record in a call to the IDataObject’s GetData method:

var Medium : TStgMedium; Handle : THandle; ... if Succeeded (FDataObject.GetData (FormatEtc, Medium)) then THandle := Medium.hGlobal

Succeeded is a function that returns true if the call was successful to GetData. The call returns a TStgMedium record, and this record (if you’re still with me) looks like this:

type TStgMedium = record tymed: Longint; case Integer of 0: (hBitmap: HBitmap; unkForRelease: Pointer{IUnknown}); 1: (hMetaFilePict: THandle); 2: (hEnhMetaFile: THandle); 3: (hGlobal: HGlobal); 4: (lpszFileName: POleStr); 5: (stm: Pointer{IStream}); 6: (stg: Pointer{IStorage}); end;

and so you can see depending on the data you’re trying to pick up differing types of handles or interfaces are made available.

And so to get the text is to obtain it from the global memory block:

So to recap and to fill in a little more detail, the steps needed to drop some text onto a memo control are:

• Create an IDropTarget interface for the TMemo control and register the drop target interface with windows.
• When the DragOver event is fired, look at the dataobj to see if CF_TEXT is available (this is done by obtaining the enumeration interface and iterating through the available formats looking for CF_TEXT). If it is available the give feedback to the user for a move or copy drop effect, or no drop allowed if CF_TEXT is not present.
• When the Drop event is fired, again iterate looking for CF_TEXT, but now use the returned FormatEtc to obtain a StgMedium, and the the StgMedium to get the string from the global memory block.
• Insert the text into the TMemo at the drop point.
 

Example Project

with some text dragged from Word and dropped onto the memo:

This demonstration is self-contained and does not have any components to install.


Chapter 3 – Enhancements and More Formats

Enhancing the TMemo Drop Target

At the end of Chapter 2 I gave the code for a simple implementation for allowing a TMemo to become a drop target for text dragged from other applications. I suggested dragging text from Wordpad or Word to see the effect. Before moving on to other formats of data that can be transferred by drag and drop operations I want to add two enhancements to the demo (and correct a leak):

1. Adding user feedback – in this case it is a bit of a guess where the text you are moving will land up – so we’ll add a caret (the text cursor) which moves around the memo showing where the text will be added, rather than where the cursor is actually pointing.

2. The memo component can contain text wider and longer than the actual displayed size, so a means is needed to scroll the text in the memo so that all insertion points with the memo become available even if not visible when the drag starts.

3. I allowed a resource leak to slip through in the previous demo, so I’ll correct that mistake here.

Both of the enhancing techniques – user feedback and scrolling – will turn up when trying to drop data onto many types of control. You’ll have seen both in use in Explorer where dragged files or groups of files are shown in outline, and the directory pane can be scrolled while dragging a file by moving the cursor close to the edge of the treeview.

Adding user feedback

The code for this has been added to the editdrop project. The key action takes place in the DragOver method. I won’t go into the detail here as the code is well commented, but the essence is that the mouse position is used to calculate the position where the text will be inserted and a thin grey caret is used to indicate the spot. As the user moves the mouse around, the caret jumps between insertion points. The overall effect looks like this (look near the cursor in about the centre of this image):


User feedback is a place where drag and drop can be transformed from something nice but so-so, to something which is a real hit – it can add a lot to the intuitive nature of drag and drop.

Adding Scrolling

The need here is to detect if the cursor is rested in a narrow region inset around the border of the control. If the cursor is left here long enough then the control can be scrolled in that direction. This figure shows the inset region highlighted in red (in practice it wouldn’t look like this of course):

You should also return the dwEffect parameter containing the effect deScroll so that the data source knows the target is scrolling. The main effect you will see is the cursor changes from a drop cursor to a special scrolling/drop cursor.

Windows defines several constants to describe the inset region and the time delays needed to make scrolling appear consistent between applications. These are:

//Default drag constants as Delphi style (originals in ActiveX) const ddScrollInset = DD_DEFSCROLLINSET; // = 11 ddScrollDelay = DD_DEFSCROLLDELAY; // = 50 ddScrollInterval = DD_DEFSCROLLINTERVAL; // = 50

The ddScrollInset is the width/height of the inset region, ddScrollDelay is the time in mS that you should wait before scrolling starts and ddScrollInterval is the time between each scrolling action. You’ll notice that both time delays are less that the clock tick interval of 55mS, so in practice the intervals will be 55mS. But apart from consistency there is no real reason to stick with these constants, you could use variables and initialise them from values stored in the registry and allow some user customisation of them.

Again, the code is well commented so I won’t look at it in detail here. But the basic process is:

1. Set a tick timer value (integer variable) to zero in the DragEnter method to show that no scrolling has started
2. The rest of the action takes place in DragOver. First look to see if the cursor is over one of the memo’s scroll bars, if so disallow the drop, hide the caret way and do no more.
3. If you’re not over a scrollbar, then compare the mouse position with the memo’s left edge plus ddScrollInset, (actual text) right edge minus ddScrollInset, Top plus ddScrollInset and (actual text) Bottom less ddScrollInset. A flag is set if any conditions are true to indicate which direction (or directions if the mouse is in a corner) the memo should scroll.
4. Set the timer to the current tick count (using the API call GetTickCount) plus the ddScrollDelay value. Return deScroll in the dwEffect parameter ored with the drop effect you currently have.
5. Don’t do anything more until the tick count has passed the value you determined in step 4. Now scroll the memo (for example I use Memo.Perform (WM_HSCROLL, SB_LINERIGHT, 0); to scroll the memo to the right). Now reset the tick timer to the current tick count plus ddScrollInterval.
6. Repeat 5 until the mouse leaves the inset area or the drop occurs or is cancelled.

You may be surprised at the amount of code needed to do a simple drop of text onto a memo, with feedback and with scrolling. This is because I’m trying to illustrate the principles of being a drop target before encapsulating all of the behaviour I want in a number of components.

Resource Leak Fixed (Oooops)

Each time you obtain a TStgMedium by calling, say GetData, usually you become responsible for cleaning up and freeing the data after using it. This is remarkably easy to do as you just call the API function ReleaseStgMedium passing the TStgMedium structure as its single parameter. I have added a FreeMedium method to the TEnumFormats class that accomplishes this for you. It is necessary to call it after you are finished with the data; I have protected resources by using a try…finally block to ensure that FreeMedium gets called.

This just about wraps it up for the memo demo for now, I’ll come back to it when I get on to wrapping the drop code into components and then again to make it act as a drop source for text.

More Formats

A number of (clipboard) formats that can be transferred using drag and drop operations are, like plain text, trivially simple to retrieve by adding new methods to the format enumerator class used in the editdrop project:

CF_FILENAME is just a piece of plain text so it is easy to add:

//--- FILENAME --- // Returns a filename or empty if not present function TEnumFormats.Filename : string; begin Result := SomeText (CF_FILENAME) end; // Returns true if a filename is available function TEnumFormats.HasFilename : boolean; begin Result := HasFormat (CF_FILENAME) end;

Similarly CF_OEMTEXT is just plain text so again it is easy to add an OemText and HasOemText methods. CF_RTF format is plain text except that it contains rich text formatting instructions.

More Formats, More Formats

So far I have dealt with text formats, another series of formats deal with lists of names

1) CF_PRINTERS - printer friendly names dropped on the target

2) CF_HDROP - a list of filenames dropped on the target. This is superior to the CF_FILENAME format as it lists all names dropped, whereas CF_FILENAME contains only the first name

3) CF_FILENAMEMAP - this contains a list of filenames with a one to one correspondence with the CF_HDROP list. It is used to provide replacement filenames.


CF_PRINTERS and CF_HDROP work by the TStgMedium providing a hDrop handle from which you can obtain the filenames using the DragQueryFile API function to obtain the file or printer names. CF_FILENAMEMAP is a list of zero terminated strings with an empty string marking the end of the list.

Drop Target Application

In order to investigate the data available from various drag sources I'll introduce an application that lists the available formats and renders those it knows for viewing:

This screenshot shows the application having just had a section dragged from Word, and so shows the available formats, and the rich text format rendered in a TRichEdit component. If you want to experiment with this application I suggest:

1) Drop text dragged from Word or WordPad;
2) Drop files dragged from Explorer
3) Drop files dragged from the recycle bin
4) Drop printer names (actually need to link) from the control panel.

This application can also be used to look at the clipboard, copy something, from say Delphi onto the clipboard and press the button. You’ll see a wide range of formats as you try these ideas, only a few of which this application renders.

Incidentally, the code for access the clipboard is trivially simple:

procedure TDropTargetForm.PasteFromClipboardToolButtonClick(Sender: TObject); var DataObj : IDataObject; begin if Succeeded (OleGetClipboard (DataObj)) then begin DataDropped(DataObj); DataObj := nil end end;

This is because OLE Drag/Drop and clipboard operations are intended to be implemented together, having implemented one, the other becomes simple.
Chapter 4 – More Formats

At the end of Chapter 3 I expanded the format enumerator to receive more text formats, and a range of list formats. In this chapter, I'll expand the enumerator to obtain many more formats that are available from the clipboard and during drag and drop operations.

In all cases the expansion of the enumerator is made in the same way:

1) By adding a HasXXXXX : boolean function which returns true if the desired format is available.

2) By adding a XXXXX : TSomeType function which returns the actual data to the user (and it is the user's responsibility to destroy the data afterwards).

Finally, the data object viewer is extended to display these new formats.

Adding an Enhanced metafile

The storage medium, in this case, contains a specific handle, in this case to enhanced metafile. This handle is copied and applied to the handle property of a Delphi TMetafile.

function TEnumFormats.HasEnhMetafile : boolean; begin Result := HasFormat (CF_ENHMETAFILE) end; function TEnumFormats.EnhMetafile : TMetafile; begin Result := TMetafile.Create; if HasEnhMetafile then try Result.Handle := CopyEnhMetafile (EnhMetafileHandle, nil) finally FreeMedium end end;

This enhanced metafile is returned in the native scaling (i.e. in himetric units), you may also see an "EMF Screen Picture" format from some applications. This format is scaled to the original size and is often more presentable, the native metafile being rather large when displayed without scaling. The enumerator has two similar routines to the above for the EMF Screen Picture format.

Note also that the data is copied – we cannot hang onto the original handle that will disappear when FreeMedium is called.

Adding a Metafile

The Delphi TMetafile class is, by default (and also ignoring Delphi 1 here), an enhanced metafile. And so to bring a Windows 3.1 metafile in we must convert it to an enhanced one first. Additionally, the data handle returned is to a TMetafilePict record, which inter alia contains the handle to the metafile itself. So the code to copy the metafile is a bit more complex than the case above:

function TEnumFormats.HasMetafile : boolean; begin Result := HasFormat (CF_METAFILEPICT) end; function TEnumFormats.Metafile : TMetaFile; var Block : hGlobal; BlockData : PMetafilePict; MetafileSize : integer; TempBits : PChar; EnhHandle : hEnhMetafile; begin Result := TMetafile.Create; if HasMetafile then begin // get a pointer to the TMetafilePict //data passed by the DataObject Block := MetafileHandle; try BlockData := GlobalLock (Block); try // find out how much storage is needed; //allocate memory for it MetafileSize := GetMetafileBitsEx( BlockData^.hMF, 0, nil ); GetMem (TempBits, MetafileSize); try // retrieve the metafile data GetMetafileBitsEx( BlockData^.hMF, MetafileSize, TempBits ); // convert the data to an enhanced metafile // and obtain a handle to it EnhHandle := SetWinMetafileBits( MetafileSize, TempBits, 0, BlockData^ ) finally FreeMem (TempBits, MetafileSize) end; // pass the enhanced handle to the TMetafile if EnhHandle <> 0 then Result.Handle := EnhHandle finally GlobalUnlock (Block) end finally FreeMedium end end end;

Adding a Bitmap

The storage medium record can contain a handle to a GDI object such as a bitmap. This handle is used to obtain a CF_BITMAP format:

function TEnumFormats.HasBitmap : boolean; begin Result := HasFormat (CF_BITMAP) end; function TEnumFormats.Bitmap : TBitmap; begin Result := TBitmap.Create; if HasBitmap then try Result.Handle := CopyImage( GDIHandle, IMAGE_BITMAP, 0, 0, LR_COPYRETURNORG ) finally FreeMedium end end;

Adding a device independent bitmap (DIB)

A DIB is provided as a handle to a TBitmapInfo structure in memory. This structure contains all the data we want but not in a format where it can be copied and applied to the handle property of a Delphi TBitmap class directly. The route I've used is not particularly efficient as I write the data out to a memory stream putting a bitmap file header on the front of the data. This can be read back into the Delphi TBitmap using LoadFromStream. It is not efficient and will be problematic with large bitmaps as up to four copies of the bitmap may be in memory together (the original, the clipboard or drag copy, our memory stream copy and the TBitmap final copy). So if you're planning on using large bitmaps, be warned!

function TEnumFormats.HasDIB : boolean; begin Result := HasFormat (CF_DIB) end; function TEnumFormats.DIB : TBitmap; var InfoSize : integer; Info : PBitmapInfo; InfoHandle : hGlobal; MemoryStream : TMemoryStream; BMF : TBitmapFileheader; begin Result := TBitmap.Create; if HasDIB then begin InfoHandle := GlobalHandle; try InfoSize := GlobalSize (InfoHandle); Info := GlobalLock (InfoHandle); try ZeroMemory (@BMF, sizeof (TBitmapFileheader)); BMF.bfType := $4D42; MemoryStream := TMemoryStream.Create; try MemoryStream.Write (BMF, sizeof (BMF)); MemoryStream.Write (Info^, InfoSize); MemoryStream.Seek (0, 0); Result.LoadFromStream (MemoryStream) finally MemoryStream.Free end finally GlobalUnlock (InfoHandle) end finally FreeMedium end end end;

Where are you now?

I've added support for the CF_LOCALE format that returns a LCID (Large Combustion Installations Directive? I think not) value describing your locale, or more precisely, the locale of the thread from which the data was obtained. You'll see applications like Delphi and NotePad give a locale value when you clip text.

function TEnumFormats.HasLocale : boolean; begin Result := HasFormat (CF_LOCALE) end; function TEnumFormats.Locale : LCID; var H : hGlobal; P : PWord; begin Result := 0; if HasLocale then begin H := GlobalHandle; if FMediumValid then try P := GlobalLock (H); try Result := P^ finally GlobalUnlock (H) end finally FreeMedium end end end;

A locale is thread local, so I would guess you could compare the locale value obtained to that of your current thread to ensure character sets match.


Unicode Text

Another aspect of "where are you?" is the need to be able to handle unicode text. This can be available using the CF_UNICODE format, where a wide character string is available as a global handle. It takes a little work to copy this string into a memory area (allocated using CoTaskMemAlloc) and the functions here return a pointer to it (of type PWideChar). You should dispose of this pointer using CoTaskMemFree.

function TEnumFormats.SomeWideText (Format : TClipFormat) : PWideChar; function WCopyStr (Source : PWideChar) : PWideChar; function WStrLen (Str: PWideChar): integer; begin Result := 0; while Str [Result] <> #0 do inc (Result) end; var Size : longword; begin Size := (WStrLen (Source)+1) * sizeof (WideChar); Result := CoTaskMemAlloc (Size); if Result = nil then OutOfMemoryError; Move (Source^, Result^, Size) end; var H : hGlobal; P : PWideChar; begin Result := nil; // check that text is available *AND* position // the enumerator on the text data // get the global handle to the data if HasFormat (Format) then begin H := GlobalHandle; if FMediumValid then try if H <> 0 then begin // it's a pointer to a null terminated string P := GlobalLock(H); try Result := WCopyStr (P) // get our copy finally GlobalUnLock (H) // let it go end end finally FreeMedium // free the storage medium end end end; function TEnumFormats.HasWide : boolean; begin Result := HasFormat (CF_UNICODETEXT) end; function TEnumFormats.Wide : PWideChar; begin Result := SomeWideText (CF_UNICODETEXT) end;

Drop Viewer Enhanced

The data object viewer is extended to cope with these new formats. You can see from this object dragged from Excel that a rich set of data formats become available.

One short cut in the data object viewer is that the widechar (unicode) text display does nothing more than convert it to a boring (ANSI) string for display purposes.


Next steps?

In the next Chapter, I will again extend the data enumerator, this time into the wonderful (?) world of shell objects. Later on I'll show how these can be used to enhance user feedback during file drag operations. I shall also add a few of the spreadsheet clipboard formats (CF_SYLK, CF_DIF and CF_CSV) which are all text based. And lastly, I'll start to explore structured storage by including a hex dump viewer for formats offering an IStream interface.


Chapter 5 – And yet more formats

Chapter 4 expanded the enumerator to cover many of the graphical formats available. But I need to take the risk of boring you with yet more data formats before getting on with describing some components. Specifically I'm going to cover:

1) Palettes – I should have included this one with the graphical formats so I'll make up for that omission now;

2) Shell Objects – these are provided when you drag and drop (or cut and paste) objects from the windows interface such as the desktop. These formats are handy when deciding how to give the user some feedback on the potential drop effect;

3) Add some spreadsheet formats; and

4) Adding a Hex Dump Viewer to peek at the contents of the clipboard offering an IStream interface as the means of looking at its data.

Finally, the data object viewer is expanded to cover all these additions.

Palette Format

The storage medium, in this case, contains a GDI handle of a hPalette. Thanks to a routine in the graphics unit it is trivially simple to copy the handle (remember that we must copy any of the data offered by the data object because it will disappear when the call to FreeMedium is made):

//--- PALETTE --- function TEnumFormats.HasPalette : boolean; begin Result := HasFormat (CF_PALETTE) end; function TEnumFormats.Palette : hPalette; begin if HasPalette then try // in graphics.pas, very handy Result := CopyPalette(GDIHandle) finally FreeMedium end else Result := 0 end;

Shell Objects

An object dragged from the desktop, or other parts of the Windows operating system, often have a range of formats provided that describe their contents or original position. These formats include CF_OBJECTDESCRIPTOR, CF_LINKDESCRIPTOR, CF_OBJECTPOSITIONS, CF_IDLIST, CF_FILEGROUPDESCRIPTOR and CF_FILECONTENTS.

I'm not going to cover all of these formats now, but will eventually cover them all (if you've the patience).

Descriptors

The CF_OBJECTDESCRIPTOR and CF_LINKDESCRIPTOR formats return almost identical information and do in fact return the data in the same record. You'll find this record, called TObjectDescriptor declared in ActiveX.pas. There two problems with the record that make it difficult to use. The first problem being that the strings are wide character strings and second is that you're given the location of the strings as offsets to them beyond the end of the record. Perhaps in C you can write some niffty code to access these strings but I much prefer to have them available as Delphi string types. So, I've redefined the record as:

type TObjectDescriptor = packed record CLSID : TCLSID; DrawAspect : integer; Size, Point : TPoint; Status : integer; FullUserTypeName, SrcOfCopy : string; end;

and provided a function to translate the ActiveX declared version that a data object offers you into this more Delphish version. Obtaining the record is then easy:

// Translates an ActiveX Object descriptor //into a Delphi object descriptor (IMHO) function XlatObjectDescriptor( const OD : ActiveX.TObjectDescriptor ) : TObjectDescriptor; begin with Result do begin CLSID := OD.CLSID; DrawAspect := OD.dwDrawAspect; Size := OD.Size; Point := OD.Point; Status := OD.dwStatus; FullUserTypeName := WideCharToString( PWideChar(integer (@OD) + OD.dwFullUserTypeName) ); if OD.dwSrcOfCopy = 0 then SrcOfCopy := 'Unknown Source' else SrcOfCopy := WideCharToString( PWideChar(integer (@OD) + OD.dwSrcOfCopy) ) end end; // Fill a TObjectDescriptor record // from an ActiveX.TObjectDescriptor used // by CF_OBJECTDESCRIPTOR and CF_LINKSCRDESCRIPTOR function TEnumFormats.SomeDescriptor( Format : TClipFormat ) : TObjectDescriptor; var Block : hGlobal; BlockData : ActiveX.PObjectDescriptor; begin ZeroMemory (@Result, SizeOf (TObjectDescriptor)); if HasFormat (Format) then try Block := GlobalHandle; BlockData := GlobalLock (Block); try Result := XlatObjectDescriptor (BlockData^) finally GlobalUnlock (Block) end finally FreeMedium end end; //--- OBJECT DESCRIPTOR --- function TEnumFormats.ObjectDescriptor : TObjectDescriptor; begin Result := SomeDescriptor (CF_OBJECTDESCRIPTOR) end; function TEnumFormats.HasObjectDescriptor : boolean; begin Result := HasFormat (CF_OBJECTDESCRIPTOR) end; //--- LINKSRCDESCRIPTOR function TEnumFormats.LinkDescriptor : TObjectDescriptor; begin Result := SomeDescriptor (CF_LINKSRCDESCRIPTOR) end; function TEnumFormats.HasLinkDescriptor : boolean; begin Result := HasFormat (CF_LINKSRCDESCRIPTOR) end;

A full description of the elements that make up the record can be found in ole.hlp but also you can get a good idea of what is returned by dropping a wide range of objects on the viewer and watching the effect. By way of an example, here's a static metafile dropped onto the data object viewer with the elements of the TObjectDescriptor record displayed in a ListView:

ID List Format

This interesting format returns a series of Pointers to Item ID List structures (PItemIDList) for objects dropped from windows. The structures can be used to obtain information about the object such as it's name and the icon that represents it. I'm not going to go into everything about structures, but will describe the use in the viewer as an illustration.

The code used to manipulate the PItemIDLists, such as creating or copying them, I have copied from Brad Stowers' TSystemTreeView component (http://www.aye.net/~bstowers/delphi/) that uses them extensively. The code is reproduced here with his kind permission. You can learn a lot about them by examining his code (I did!).

But back to getting the data from a data object. When I first looked at the help for the CF_IDList format, I was a bit baffled by the description:

This structure corresponds to the CF_IDLIST clipboard format.

typedef struct _IDA {
UINT cidl; // number array elements
UINT aoffset[1]; // see below
} CIDA, * LPIDA;


Member aoffset

Array of offsets relative to the beginning of the CIDA structure. The first element is the offset of the ITEMIDLIST structure for a folder (absolute from the root). Subsequent elements are offsets of ITEMIDLIST structures for file objects (relative from the parent folder).


Perhaps to you it's more obvious! Eventually it dawned on me what this all meant and decided to make this a bit more user-friendly. I take each element from this list and place the PItemIdList as the data element of a string list, where string part is either the path to the object, or the screen display name. Because the data has been allocated memory, it must be freed before the string list is freed. And so a utility procedure, FreeIDList, is provided to do just that.

To illustrate the use of a PItemIDList

var List : TStringList Loop : integer; ... function GetIconIndex( IDList: PItemIDList; Flags: UINT ): integer; var SFI: TSHFileInfo; begin if SHGetFileInfo( PChar(IDList), 0, SFI, SizeOf(TSHFileInfo), Flags ) = 0 then Result := -1 else Result := SFI.iIcon end; . . . if HasIDList then begin List := FEnumFormats.IDList; with List do try if Count > 0 then RootLabel.Caption := SysUtils.Format('Root folder: %s', [IDList[0]]); IDListView.Items.Clear; for Loop := 1 to IDList.Count - 1 do with IDListView.Items, Add do begin Caption := ExtractFilename (IDList[Loop]); ImageIndex := GetIconIndex( PItemIDList(IDList.Objects[Loop]), SHGFI_PIDL or SHGFI_SYSICONINDEX or SHGFI_LARGEICON ) end finally FreeIDList(List) end end

This code is taken from the data object viewer. It places the first element from the string list, the "root folder", in a label and then the remaining elements as captions in a TListView. The icon for each element is obtained using the SHGetFileInfo API call that can be used to obtain a lot more other useful information as well. For half a dozen objects dragged from the desktop at random, the net effect is something like this:

Object Positions

The global memory block returned by CF_OBJECTPOSITIONS contains an array of TPoint structures. The first structure specifies the screen coordinates of a group of shell objects and the remaining structures specify the relative offsets of each item in the group (measued in pixels). However, there is no indication given of the number of items being returned. I think you can always expect two, but can't be sure from CF_OBJECTPOSITIONS alone the total count. So I have used the count returned by CF_IDLIST (which always partners a CF_OBJECTPOSITIONS) to retrieve the correct number in the array. To make life easier I have again translated the C structure into a Delphi record (you'll notice I've introduced some Delphi 4/5 syntax here, my apologies to Delphi 2/3 users, but I've paid my ££££££s and intend to make full use of the syntax advantages):

type PObjectPositions = ^TObjectPositions; TObjectPositions = packed record Count : integer; Group : TPoint; Offsets : array of TPoint // Delphi 4: Wow !!!!! end; function TEnumFormats.ObjectPositions : TObjectPositions; var Block : hGlobal; BlockData : PPoint; IDA : PIDA; Loop : integer; begin ZeroMemory (@Result, SizeOf (TObjectPositions)); if HasIDList then begin Block := GlobalHandle; try IDA := GlobalLock(Block); try Result.Count := IDA^.cidl finally GlobalUnlock(Block) end finally FreeMedium end end else Result.Count := 0; if HasObjectPositions and (Result.Count > 0) then begin Block := GlobalHandle; try BlockData := GlobalLock(Block); try Result.Group := BlockData^; inc (BlockData); SetLength (Result.Offsets, Result.Count); for Loop := 0 to Result.Count - 1 do begin Result.Offsets[Loop] := BlockData^; inc(BlockData) end finally GlobalUnlock (Block) end finally FreeMedium end end end; function TEnumFormats.HasObjectPositions : boolean; begin Result := HasFormat (CF_OBJECTPOSITIONS) end;

So you should see in this code the two step process, obtain the count from the CF_IDLIST then copy the positions into the record to return. The dynamic Delphi array easing us by the problem of having to create and destroy our own array.

Using the data array is relatively simple, but I'm going to skip it here, you'll have to wait 'til I get back round to discussing giving user feedback.

Some Spreadsheet Formats

I have also added support for CF_SYLK, CF_CSV and CF_DIF that are text only spreadsheet formats. Obtaining them is simple and you can look at the demonstration code to see how it is done. Personally, I find the CF_CSV (comma separated values) the easiest format to obtain data from.

IStream, You stream, Who streams?

One of the problems with old style clipboard operations is that it is only possible to pass data as a global memory handle. So if you want to cut and paste a large bitmap as a CF_BITMAP it is passed as a memory object. These operations can gobble memory as multiple copies of the same information can be simultaneously present. To overcome some of these difficulties, the OLE based clipboard operations (and also drag and drop) can pass data as a stream interface (more complex data structures can be passed as an IStorage interface but that's beyond my scope at the moment). This has the advantage that the place the actual data resides (in memory, in a database, on disc, on a remote server) is completely independant of the data transfer mechanism itself. You don't actually care where the data is, but can call on it through the IStream interface. If you have played with the data object viewer you will have seen cases where the Medium is reported to be via an IStream:

This example is a block of text copied to the clipboard from the Borland IDE editor.

The IStream interface is declared in ActiveX.pas as:

type IStream = interface (IUnknown) ['{0000000C-0000-0000-C000-000000000046}'] function Read( pv: Pointer; cb: Longint; pcbRead: PLongint ): HResult; stdcall; function Write( pv: Pointer; cb: Longint; pcbWritten: PLongint ): HResult; stdcall; function Seek( dlibMove: Largeint; dwOrigin: Longint; out libNewPosition: Largeint ): HResult; stdcall; function SetSize(libNewSize: Largeint): HResult; stdcall; function CopyTo( stm: IStream; cb: Largeint; out cbRead: Largeint; out cbWritten: Largeint ): HResult; stdcall; function Commit(grfCommitFlags: Longint): HResult; stdcall; function Revert: HResult; stdcall; function LockRegion( libOffset: Largeint; cb: Largeint; dwLockType: Longint ): HResult; stdcall; function UnlockRegion( libOffset: Largeint; cb: Largeint; dwLockType: Longint ): HResult; stdcall; function Stat( out statstg: TStatStg; grfStatFlag: Longint ): HResult; stdcall; function Clone(out stm: IStream): HResult; stdcall; end;

The IStream interface is not dissimilar to the TStream class: it contains methods to read and write bytes, seek a location, set the stream size and to copy the contents. And then there are methods not present in TStream. The Commit and Revert methods enable you to have transacted streams; Lock and Unlock enable a variety of read and write protection of regions of the stream; Stat provides a lot of information such as creation date/time, stream size and permission flags; and finally clone can be used to duplicate the whole stream. What I want to do is simply copy the stream conents and display the copy in a hex viewer.

NB: You'll find a file called hexdump.pas in c:\program files\borland\delphi?\demos\resxplor and you need to it that to your project directory to get this chapter's version of the dataobject viewer to compile.

Given an IStream Interface in the variable Source, it is easy to copy the contents into memory:

var Source : IStream; StatStg : TStatStg; Readed : integer; NewPos : Int64; Memory : pointer; Size : integer; ... // Get size of stream Source.Stat (StatStg, STATFLAG_NONAME); Size := StatStg.cbSize; // Obtain memory GetMem (Memory, Size); // Get data Source.Seek (0, 0, NewPos); Source.Read (Memory, Size, @Readed);

This obtains the size of the stream using the Stat method, obtains the memory block to contain the data, and then reads the stream across. In the case of the dataobject viewer, the memory and size variables are passed to the hex viewer for display.

What Next

In this chapter, I've enhanced the dataobject viewer adding more formats and the ability to look at some IStream storage contents. There is still some way to go with the project as there are still interesting formats to cover and also, the IStorage interface to peek at. But in the next Chapter I want to rationalise the last 5 into a few units and components. Effectively starting to provide useful components to build drop sensitive applications.


Chapter 6 – Consolidation of Drop Target Considerations

Over the last few chapters I have expanded the data object viewer and showed how many different data formats can be obtained. In passing, I showed that the clipboard can read using exactly the same code. In the earlier chapters, I illustrated two important features of drag and drop, these were user feedback (to help the user understand what is going on) and autoscrolling (to bring controls into view during a drop operation). The data object viewer has grown in a bit of a muddled way and so now I thought it would be useful to bring together the various strands into a series of objects and components.

So the purpose of this chapter is to describe:

• Drop Target objects
• How to integrate externally dragged data with Delphi control (ie internal) dragged data
• Autoscrolling of controls for external and internal data

I'll be omitting the stuff which I have covered in depth already, mainly data object format enumeration and the internal workings of the autoscroller.

Drop Target Components

In writing this code I had several objectives, first for myself to learn about COM OLE and Drag'n'Drop!. Secondly to isolate the final Delphi code from all of the C styled interfaces in ActiveX.pas, OleDlg.pas and so on. The last objective means putting in some interposing classes to translate the various function calls, and redefining a lot of constants and enumeration into Delphi styled ones. Functionally this adds very little, in fact it can be detrimental as often calls to methods are made which do nothing other than translate names.

Since many of the interfaces are going to be represented by VCL components, my first step was to redefine the COM interface parts of TComponent by declaring a TInterfacedComponent which is descended from TComponent and IUnknown. You'll see I've used the "reintroduce" keyword to suppress the warning messages (D3 users will need to delete the keyword and live with the warnings). This simply gives each component the needed IUnknown interface:

type TInterfacedComponent = class (TComponent, IUnknown) private function _AddRef: integer; reintroduce; stdcall; function _Release: integer; reintroduce; stdcall; protected FRefCount : integer; function QueryInterface( const IID: TGUID; out Obj ): HResult; reintroduce; stdcall; public procedure BeforeDestruction; override; function AddRef : integer; function Release : integer; property RefCount: integer read FRefCount; end;

The next step in my anti-C campaign is to encapsulate the IDropTarget interface into a class which is used to redefine the call methods and translate the key parameters into Delphi ones (for example grfKeyState:Longint is translated to State:TShiftState). The translations are declared "abstract" a will be overriden in all descendant (and functional) versions:

type TBaseDropTarget = class (TInterfacedComponent, IDropTarget) //IDropTarget function DragEnter( const DataObj: IDataObject; grfKeyState: Longint; pt: TPoint; var dwEffect: Longint ) : HResult; overload; stdcall; function DragOver( grfKeyState: Longint; pt: TPoint; var dwEffect: Longint ): HResult; overload; stdcall; function DragLeave : HResult; overload; stdcall; function Drop( const DataObj: IDataObject; grfKeyState: Longint; pt: TPoint; var dwEffect: Longint ): HResult; overload; stdcall; private FDataObject : IDataObject; protected procedure DragEnter( DataObject : IDataObject; State : TShiftState; Pt : TPoint; var Effect, Result : integer ); overload; virtual; abstract; procedure DragOver( DataObject : IDataObject; State : TShiftState; Pt : TPoint; var Effect, Result : integer ); overload; virtual; abstract; procedure DragLeave( var Result : integer ); overload; virtual; abstract; procedure Drop( DataObject : IDataObject; State : TShiftState; Pt : TPoint; var Effect, Result : integer ); overload; virtual; abstract; property DataObject : IDataObject read FDataObject; end;

Also, note that I've used the overload keyword so that the IDropTarget method name and the new Delphi abstract name are the same (D3 users will need to delete the overload keyword and change the overloaded names to something like DoXXXXX).

I'll take one method as an illustration, the rest are all similar:

function TBaseDropTarget.DragEnter( const DataObj: IDataObject; grfKeyState: Longint; pt: TPoint; var dwEffect: Longint ): HResult; begin Result := NOERROR; try dwEffect := DROPEFFECT_NONE; FDataObject := DataObj; DoDragEnter( DataObj, KeysToShiftState(grfKeyState), Pt, dwEffect, integer(Result) ) except Result := E_UNEXPECTED; raise end end;

The incoming DragEnter call is reflected in a call to the abstract DoDragEnter call. Note also the try..except..end arrangement which ensures and exceptions are trapped and the "Unexpected" error message is returned.

The final level is to provide the non-interface related stuff which make the DropTarget easy to use in a Delphi environment, I've described it all earlier. But you'll see that the abstract classes are not overriden yet:

type TStdDropTarget = class (TBaseDropTarget) private //omitted public constructor Create (AOwner : TComponent); override; destructor Destroy; override; procedure Loaded; override; procedure Revoke; virtual; procedure Register; virtual; property Parent : THandle read FParent; property Handle : THandle read FHandle; published property Active : boolean read FActive write SetActive; end;

Descendant One

This DropTarget component emulates the Delphi internal drop arrangements and makes dropped objects from outside the Delphi application appear through the OnDragOver and OnDragDrop event handlers. The "Source" term in these handlers in a pointer to a TComDragObject instance that can be used to obtain the IDataObject interface and key states, and return the desired Effect and any error result. It operates by allowing the form to be the registered target for drops and then looks at all of the components on the form to see on which the mouse is positioned.

The component declaration is bland, it just overrides the four abstract methods:

type TDelphiDropTarget = class (TStdDropTarget) private protected procedure DoDragEnter( DataObject : IDataObject; State : TShiftState; Pt : TPoint; var Effect, Result : integer); override; procedure DoDragOver( DataObject : IDataObject; State : TShiftState; Pt : TPoint; var Effect, Result : integer ); override; procedure DoDragLeave(var Result : integer); override; procedure DoDrop( DataObject : IDataObject; State : TShiftState; Pt : TPoint; var Effect, Result : integer ); override; public published end;

The interesting bit is the TComDragObject as this is supplied the controls OnDragOver and OnDragDrop events:

type TComDragObject = class (TBaseDragControlObject) private public destructor Destroy; override; property DataObject : IDataObject read FDataObject; property Effect : integer read FEffect write FEffect; property Result : integer read FResult write FResult; property State : TShiftState read FState; end;

One of the advantages of this method of dropping data on a control is that you can get both the internally Delphi supplied drop operation and the external, COM drop operation to work through the same interface. There is a bit of testing to do to see where the data has come from but you can get it to work. To illustrate how to do this I have provided a demo called TVDrop

This illustrates Delphi drag and drop combined with dropping text (text, filenames and printer names) from external sources using COM. It also illustrates how to make autoscrolling of Delphi drag and drop work.

Descendant Two

This descendant is used to surface drag and drop calls through the interface into Delphi events. So it works by overriding the abstract methods with new ones that call event handlers if provided, or else some default handling is provided. This descendant makes the whole form sensitive to drops and includes an auto scroller (described later) within.

type TOnDragEvent = procedure( Sender : TObject; DataObject : IDataObject; State : TShiftState; MousePt : TPoint; var Effect, Result : integer ) of Object; TOnDragLeaveEvent = procedure( Sender : TObject; var Result : integer ) of Object; TFormDropTarget = class (TStdDropTarget) private //omitted protected procedure DragEnter( DataObject : IDataObject; State : TShiftState; Pt : TPoint; var Effect, Result : integer ); override; procedure DragOver( DataObject : IDataObject; State : TShiftState; Pt : TPoint; var Effect, Result : integer ); override; procedure DragLeave(var Result : integer); override; procedure Drop( DataObject : IDataObject; State : TShiftState; Pt : TPoint; var Effect, Result : integer ); override; public constructor Create(AOwner : TComponent); override; destructor Destroy; override; published property AutoScroll : boolean read FAutoScroll write FAutoScroll; property Scroller : TAutoScroller read FAutoScroller write FAutoScroller; property OnDragEnter : TOnDragEvent read FOnDragEnter write FOnDragEnter; property OnDragOver : TOnDragEvent read FOnDragOver write FOnDragOver; property OnDragLeave : TOnDragLeaveEvent read FOnDragLeave write FOnDragLeave; property OnDrop : TOnDragEvent read FOnDrop write FOnDrop; property OnScroll : TOnScrollEvent read GetScrollEvent write SetScrollEvent; end;

Descendant Three

This descendant overrides the Form drop version by making an individual control sensitive to drops.

TControlDropTarget = class (TFormDropTarget) private FControl : TWinControl; procedure SetControl(Value : TWinControl); protected procedure Notification( Component : TComponent; Operation : TOperation ); override; published property Control : TWinControl read FControl write SetControl; end;

The control however must be a TWinControl as we need a control with a handle.

Delphi or Event Drops?

Given two approaches- to drop via the Delphi events and to drop by our own generated events, but which should you use? I think you will need to play with both techniques and decide which works best in your application. They both have advantages and disadvantages.


AutoScrolling

Autoscrolling is used during drop operations to scroll parts of a window that may not be visible into view. The punter can't drag a scrollbar with the mouse because the mouse is dragging an object - so you have to do this for the punter. I've illustrated the technique in an earlier chapter and the full autoscrolling component is provided here. It detects the type of window (one that has scrollbars built in or provided externally) and scrolls the view accordingly.

Because autoscrolling is not provided by Delphi drag and drop operations, the TAsutoScroller can provide this service as well (the TVDrop example above illustrates this). Another demo is provided to illustrate scrolling of forms and various memos. Try dragging a desktop object around on the form!

There is one twist to autoscrolling that I've not completely understood. When I first worked with autoscrolling Windows automatically changed the cursor to a black filled in arrow when scrolling was taking place, but I installed a upgrade and the automatic cursor change has failed to work since. I've documented the effect in the code but I've not tested it enough on other machines to try and understand what is happening. It is almost as if Microsoft has decided (being all-seeing and all-powererful) that the facility was not needed - or it might just be a bug.

One last point - I've added some cursor resources to the unit and put them into the global Screen class. They can be used to provide the same cursor shapes for internal Delphi drag and drops that are used by external applications by default. This gives a better feel of consistency.

Ole Library

As well as providing the drop components, I'll now present the whole of the Ole Drag and Drop Library. Many of these units will be used in future chapters. But I might as well list out now what each unit contains:


OleConsts.pas - translates C styled constants and enumeration into Delphi styled (also obtains clipboard formats)

OleErrors.pas - handles errors generated by Ole API calls or Ole UI calls

OleInterface.pas - provides interposer classes to translate C styled interfaces into Delphi styled abstract methods

OleDnD.pas - contains (at the moment) drop target components, autoscroller and dataobject format enumeration

OleRegister.pas - Register procedures for components, component and property editors etc

OleNames.pas - translates constants and enumeration into readable strings

Chapter 7

Before moving on to source drag and drop events I'll have a quick look at the Paste Special dialog box. How you get it to appear and how you use it. It offers a form of "manual intervention" during drop operations or OLE clipboard pasting.

Chapter 7 – Using the Paste Special Dialog

Introduction

You'll all have seen and used the Paste Special dialog box...

Getting access to it is relatively simply, although first reading the definition of the structure to fill out to make it appear was like peering through the car windscreen on a very foggy English day. So I'll take you through the steps to make this dialog appear and present a wrapper component for the dialog in true Delphi style. A demo shows how to use the paste special dialog in drag and drop operations as well as the conventional use from a menu.

The Fog

To use the dialog is simple (a example rhetorical humour). You fill in a TOleUIPasteSpecial structure and call the OleUIPasteSpecial function, the function returns whether you pressed Ok or Cancel or an error code; it also returns the selections made in the dialog box back in the TOleUIPasteSpecial record. The declarations of the record and function you'll find in oledlg.pas.

The record you must fill out is:

type POleUIPasteSpecial = ^TOleUIPasteSpecial; TOleUIPasteSpecial = record { Structure Size } cbStruct: Longint; { IN-OUT: Flags } dwFlags: Longint; { Owning window } hWndOwner: HWnd; { Dialog caption bar contents } lpszCaption: PChar; { Hook callback } lpfnHook: TFNOleUIHook; { Custom data to pass to hook } lCustData: Longint; { Instance for customized template name } hInstance: THandle; { Customized template name } lpszTemplate: PChar; { Customized template handle } hResource: HRsrc; { IN-OUT: Source IDataObject on the clipboard } lpSrcDataObj: IDataObject; { IN: Array of acceptable formats } arrPasteEntries: POleUIPasteEntry; { IN: No. of TOleUIPasteEntry array entries } cPasteEntries: Integer; { IN: List of acceptable link types } arrLinkTypes: PLongint; { IN: Number of link types } cLinkTypes: Integer; { IN: Number of CLSIDs in lpClsidExclude } cClsidExclude: Integer; { IN: List of CLSIDs to exclude from list } lpClsidExclude: PCLSID; { OUT: Index that the user selected } nSelectedIndex: Integer; { OUT: Indicates if Paste or PasteLink } fLink: BOOL; { OUT: Handle to Metafile containing icon } hMetaPict: HGlobal; { OUT: size of object/link in its source may be 0, 0 if different display aspect is chosen } sizel: TSize; end;

Which takes a little time to understand. The first thing to note is that the first nine elements (cbStruct to hResource) are common to all OleUI dialogs. The remaining twelve are used by the paste special dialog. I'm not going to describe all of them in detail because you can find the explainations in the Ole Programmers Reference (ole.hlp) file. But I will cover the principles of what you do:

• tell the dialog who you are with the hWndOwner
• customise the dialog appearance with dwFlags and lpszCaption
• provide a call back procedure for all messages sent to the dialog using lpfnHook, lCustData and hInstance - this enables you to customise the appearance once the dialog has been created, or interact with it while it is modally displayed (more on this one later)
• provide an IDataObject for it to use, or if you set this to nil, the dialog will seek to get an IDataObject from the clipboard for you
• tell the dialog which clipboard formats you want to possibly accept using cPasteEntries and arrPasteEntries
• tell the clipboad which classes of data you don't want using the "Clsid" elements, and which clipboard formats you can link using the "link" elements

The OleUIPasteSpecial function returns with

• a code indicating Ok was pressed (OLEUI_OK) or Cancel (OLEUI_CANCEL) or an error code (typically 100 upwards)
• which item you selected in the listbox
• the status of the various radio and check box controls
• if asked for (and available) an object image as a metafile and the object's size

If you look in the TOleContainer source code (OleCtnrs.pas) you'll find a "hard-wired" version of using this dialog, the structure is filled-in by a whole bunch of code and the dialog called. I wanted to use the dialog more in the style of the other Delphi Dialogs. But the code in OleCtnrs.pas is instructive.

At this point I'll mention a potential error in the OleDlg.pas code. I disagree with the values set for OLEUI_ERR_OLEMEMALLOC and OLEUI_ERR_STANDARDMAX. I think they should be 100 and 116 respectively. My OleConsts unit contains the corrections and so if using both units you must ensure that the OleConsts unit is in the units clause after the OleDlg unit.

The fog clears

The wrapper component enables all of the controlling data to be set at run-time in the Object Inspector:

AutoCentre will position the dialog centrally on the screen (true) or in a windows selected spot
Caption is the dialog box caption or the text "Paste Special" if left blank
DisableDisplayAsIcon disables the checkbox of the same name and inhibits that function
Exclude is a string list of class ids (CLSIDS) to be excluded. They look something like this: {A35E20C2-837D-11D0-9E9F-00A02457621F} and you put as many as needed in the stringlist and you must include the braces { }
Formats is a Delphi TCollection of the entries that you can paste and will appear (if present) in the listbox. You edit this list using the collection editor:

Here I show an OlePasteSpecialDialog component on a form with the Formats property being edited in the Object Inspector. The fields here are:

Aspect is the appearance of the data (actual content, thumbnail, icon or printer ready)
Format is the clipboard format as a number
Medium is the transfer medium (global memory, stream etc)
Name is the clipboard format in text (Format and Name interact, so changing one, changes the other)
Options are unfortunately wide ranging, but relate mostly to linking arrangements (I suggest you look in ole.hlp)
Result and Text are the strings used in the dialog box to describe the item in the result groupbox and the format choice listbox respectively. The default value of %s will allow the Paste Special dialog box to try and obtain the name of the item. In the case of the Text format (CF_TEXT) you would set them to "text without any formatting" and "Unformatted Text" respectively.

Returning to the dialog properties:

HideChangeIcon is used to
LinkTypes consists of upto eight clipboard formats that can be used for linking
NoRefreshDataObject - the dialog will detect if the clipboard contents are changed by another application and automatically refresh the contents of the dataobject (see also StayOnClipboardChange below which must be true for this to happen), if however you have provided the dataobject, you won't want the data replaced by the new stuff on the clipboard!
Resource is the name of a dialog resource used to replace the standard format. You can use this to rearrange the dialog contents of provide new functionality. An example later in this chapter gives an example of how to do this.
SelectPaste and SelectPasteLink are used to set the radio button first selected
ShowHelp if true a help button appears
StayOnClipboardChange this is used on conjunction with the NoRefreshDataObject option. If false, the default action, when the clipboard contents change the dialog is automatically closed and an error code is returned.

A Sunny Day

Even using the Delphi Object Inspector I did not find it particularly easy to set up dialogs, or recheck the contents when they misbehaved. So, I have also written a component editor which brings up a "property editor" style dialog that enables you to fiddle with the contents.

By right-clicking the component, you'll also see a Test item in the dialog which attempts to display the dialog with the current settings in force. So you can place some data on the clipboard and then see how the dialog behaves at design time (I like Delphi a lot).

Demonstration

The demonstration you can download consists of a single form with a TRichEdit, a menu, a couple of paste special dialogs and a control drop component. The program wants text formats for the rich edit in the priority order of RTF, Text and OemText. You can load text into the TRichEdit by using the menu items of Paste (where the program selects the highest format available) or Paste Special where the dialog is invoked for you to chose. You can also drag and drop some text into the control. If you do a plain drop then, again, the program selects the best format available, but if you press the Alt key as well (there's no convention here, it's just the way I've done it) then the dialog is invoked for you to select the format of the text to use. I don't intend this to be of much use to anyone - it's the principles that I'm demonstrating.

Own Resource

One of the unusual aspect of all of the Ole UI dialogs is the ability to use your own dialog template. I'm not going to use the Paste Special dialog to illustrate this, but instead use the other Ole UI dialog you'll find the units provided. This is the OleUIBusy dialog used to tell the punter when an associated task is blocking the current operation. Don't worry about why and what this dialog is for, I'm using only as an illustration because it is much simpler than the Paste Special dialog. The plain vanilla dialog looks like this:

What I want to do, by way of an example, is to add a checkbox to this dialog (you might argue that creating a purpose form in Delphi might be quicker and easier, but you would have to program all of the "behind-the-scenes" actions that take place which might not be easy at all to deduce). The first steps are to create the resource template, I extracted the template for this dialog from oledlg.dll in order to modify it:

BUSY DIALOG LOADONCALL MOVEABLE DISCARDABLE 0, 0, 214, 76 STYLE WS_POPUP | WS_BORDER | WS_DLGFRAME | WS_SYSMENU | DS_MODALFRAME | DS_SETFONT CAPTION "Server Busy" FONT 8, "MS Shell Dlg" BEGIN DEFPUSHBUTTON "&Retry", 600, 92, 55, 53, 15, WS_TABSTOP PUSHBUTTON "&Cancel", 2, 152, 55, 53, 15, WS_TABSTOP CHECKBOX "&Grunge", 610, 32, 55, 53, 15, WS_TABSTOP LTEXT "This action cannot be completed because the other program is busy. Click the appropriate button on the task bar to activate the busy program and correct the problem.", 602, 35, 11, 167, 35, WS_GROUP ICON "", 601, 8, 18, 20, 18, SS_ICON END

I've added the CHECKBOX statement with the control id of 610, shown bold. You compile this using BRCC32 and add the resulting RES file to your form using the $R directive.

The action of clicking on the checkbox is seen by the dialog hook procedure. So you add your own hook procedure which checks and unchecks the dialog's checkbox, and saves the status in a global variable for you to use when the dialog closes, causes a beep if checked in this case. The code is thus:

var Checked : boolean; const ID_CheckBox = 610; function BusyDialogHook( Wnd : HWnd; Msg, WParam, LParam : Longint ): Longint stdcall; begin if (Msg = WM_COMMAND) and (loword(WParam) = ID_CheckBox) and (hiword (WParam) = BN_CLICKED) then if IsDlgButtonChecked(Wnd, ID_CheckBox) = 0 then begin Checked := true; CheckDlgButton(Wnd, ID_CheckBox, 1) end else begin Checked := false; CheckDlgButton(Wnd, ID_CheckBox, 0) end; Result := OleDialogHook(Wnd, Msg, WParam, LParam) end; procedure TForm1.Button2Click(Sender: TObject); begin OleBusyDialog1.Hook := BusyDialogHook; OleBusyDialog1.Execute; if Checked then Beep; end;

The demonstration just consist of a Button to invoke the dialog and the dialog component itself. The result is the appearance of the checkbox on the dialog:

Ending

I've looked at the Paste Special dialog box provided by Micro$oft in oledlg.dll. It can be invoked not only from a menu item but also by drag and drop operations, or in fact, any operation where you have an IDataObject and want to allow the punter to select the data format to use. There is much more to come on Ole UI dialogs...

Chapter 8 – Starting a Drag Source Operation ..... (well almost)

Introduction

Having looked at how to act as a drop target over the last few issues of UNDU, I think its about time I looked at sourcing a drag operation. The key API call, defined in ActiveX.pas, that gets the source operation underway is:

function DoDragDrop( DataObj: IDataObject; DropSource: IDropSource; OKEffects: Longint; var Effect: Longint ): HResult; stdcall;

To use the call you do the following

1. Decide that a drag source operation is required - this will involve some sort of button down testing
2. Provide an IDataObject object (DataObj)
3. Provide an IDropSource object (DropSource)
4. Describe what effects are permitted to be used (copy, move etc) (OkEffects)
5. The API returns whether there was an error, a drop or if the drop was cancelled (HResult)
6. On a successful drop, carryout the effect that was asked for (delete the original on move etc.) (Effect)

I'll look at each of these, but not in the order described as it better to look at the implementation of an IDataObject first.

Clipboard AND Drag&Drop

One of the principle features of drag and drop operations is much of the code can be written for clipboard operations as well as drag and drop operations. In fact, a drag and drop operation is very much the same as a copy and paste operation - its just done without the clipboard. So an important part of writing drag and drop code is to ensure you can use as much as possible of it in clipboard operations as well. And also, writing clipboard code that it can have drag and drop added. It might be that you don't currently envisage that both operations are necessary, but.....

The common area between drag and drop, and the clipboard is in preparing the dataobject. The exact same dataobject can be passed to the clipboard or in the above API call if you are careful. The problem is to write a uniform IDataObject and I'm going to spend the rest of this chapter describing some of the features of the IDataObject interface and presenting a number of components that can be used with Delphi controls that create IDataObjects containing key information from the controls.

The IDataObject interface is defined as:

type IDataObject = interface(IUnknown) ['{0000010E-0000-0000-C000-000000000046}'] function GetData( const formatetcIn: TFormatEtc; out medium: TStgMedium ): HResult; stdcall; function GetDataHere( const formatetc: TFormatEtc; out medium: TStgMedium ): HResult; stdcall; function QueryGetData( const formatetc: TFormatEtc ): HResult; stdcall; function GetCanonicalFormatEtc( const formatetc: TFormatEtc; out formatetcOut: TFormatEtc ): HResult; stdcall; function SetData( const formatetc: TFormatEtc; var medium: TStgMedium; fRelease: BOOL ): HResult; stdcall; function EnumFormatEtc( dwDirection: Longint; out enumFormatEtc: IEnumFormatEtc ): HResult; stdcall; function DAdvise( const formatetc: TFormatEtc; advf: Longint; const advSink: IAdviseSink; out dwConnection: Longint ): HResult; stdcall; function DUnadvise( dwConnection: Longint ): HResult; stdcall; function EnumDAdvise( out enumAdvise: IEnumStatData ): HResult; stdcall; end;

This is a very rich interface that has many uses other the Drag and Drop. The two methods I'm going to use here are:

* GetData - supply the requested data (what you must supply is described in the FormatEtcIn record) in the Medium
* EnumFormatEtc - provide an enumeration list of available formats

The other methods do have their uses in Drag and Drop, and I'll use them in future chapters, but for now I'm going to try and keep things simple. To implement this interface, I've done my usual thing of provinding an intermediate class the translates the C-styled names and constants, and provides and new set of Delphi methods that are all abstract, virtual and overloaded. So you'll need Delphi 4. You'll see in the code that default values are returned against all of the methods.

type TObjectBaseDataObject = class (TInterfacedObject2, IDataObject) function GetData( const formatetcIn: TFormatEtc; out medium: TStgMedium ): HResult; overload; stdcall; //...omitted protected procedure GetData( const FormatEtc : TFormatEtc; var Medium : TStgMedium; var Result : integer ); overload; virtual; abstract; //...omitted end;

This is defined in OleInterface.pas. To make a functional DataObject using the two methods descibed above we need to provide a format enumerator that can be returned when EnumFormatEtc is called, and then provide the data when GetData is called.

The next task is to write a format enumerator. In the earlier chapters, I described the IEnumFormatEtc interface and how to call it. We now need to look at it in the other direction - write one that can be called! If you look in OleCntrs.pas you'll find a simple format enumerator that the component uses. I say "simple" but it is still very usable and you may find it adequate on many occassions. The one I've written for the components here is based on having a TCollection of formatetc structures. So the Collection's "Item" becomes:

type TFormatEtcItem = class (TCollectionItem) private FFormat : TClipFormat; FName : TClipName; FAspect : TClipAspect; FMedium : TClipMediums; procedure SetName(const Value : TClipName); procedure SetFormat(const Value : TClipFormat); function GetFormatEtc : TFormatEtc; protected function GetDisplayName: string; override; public constructor Create (Collection: TCollection); override; procedure Assign (Source: TPersistent); override; published property Aspect : TClipAspect read FAspect write FAspect default caContent; property Format : TClipFormat read FFormat write SetFormat stored false; property Medium : TClipMediums read FMedium write FMedium default [cmGlobal]; property Name : TClipName read FName write SetName; property FormatEtc : TFormatEtc read GetFormatEtc; end;

This contains one TFormatEtc split up into a clipboard format (FFormat), together with its string representation (FName), the Aspect (FAspect) and storage medium used (FMedium). You can read and write these values and also obtain the whole structor by reading the FormatEtc property. The collection itself is basic, with the main functions overriden to provide results of type TFormatEtcItem:

type TFormatEtcList = class (TCollection) private FOwner: TComponent; function GetItem (Index: Integer): TFormatEtcItem; procedure SetItem (Index: Integer; Value: TFormatEtcItem); protected function GetOwner: TPersistent; override; public constructor Create (AOwner: TComponent); function Add: TFormatEtcItem; property Owner: TComponent read FOwner; property Items [Index: Integer]: TFormatEtcItem read GetItem write SetItem; default; end;

My main reasons for using a collection to hold the formats are that the format collection can be published to allow design-time alteration of the contents using the Delphi provided collection property editor. Also, I like the predefined storage interface that has methods like Clear and Add.

Finally, the collection is exposed as a IEnumFormatEtc using:

type TStdEnumFormatEtc = class (TBaseEnumFormatEtc) private FList : TFormatEtcList; FIndex : integer; protected procedure Next( Celt : integer; var FormatEtc : TFormatEtc; var Fetched, Result : integer ); override; procedure Next( var FormatEtc : TFormatEtc; var Result : integer ); overload; virtual; procedure Skip( Celt : integer; var Result : integer ); override; procedure Reset( var Result : integer ); override; procedure Clone( var Enum : IEnumFormatEtc; var Result : integer ); override; public constructor Create( List : TFormatEtcList; Index : integer = 0 ); end;

The class TBaseEnumFormatEtc you'll find in OleInterface where the C-styled names and constants etc. etc - I guess by now you'll have seen the common approach I'm using with interfaces! The Delphi side of the format enumerator you'll find in OleFormatEtc.

To illustrate the use of the enumerator with a DataObject, let's look at a simple example. The aim of this application is to take the text in the TEdit and either copy or cut the data to the clipboard using a DataObject; and a third button will be used to clear the clipboard.

The event that occurs when Copy is pressed is:

procedure TForm1.CopyButtonClick(Sender: TObject); begin if Edit1.Text <> '' then OleSetClipboard (TTestDataObject.Create(Edit1.Text)) end;

This creates a dataobject (I'll describe this next) passing the current text from the TEdit, passing the resulting DataObject to the clipboard. There is no mechanism in the code needed to free this DataObject as it is now owned by the clipboard (the system) and it will free it when it wants to.

The DataObject class declaration and constructor used here are:

type TTestDataObject = class (TObjectBaseDataObject) private FCaptured : string; FFormatEtc : TFormatEtcList; protected procedure GetData( const FormatEtc : TFormatEtc; var Medium : TStgMedium; var Result : integer ); override; procedure EnumFormatEtc( Direction: integer; var EnumFormatEtc: IEnumFormatEtc; var Result : integer ); override; //... omitted public constructor Create(const Capture : string); destructor Destroy; override; end; constructor TTestDataObject.Create(const Capture : string); begin inherited Create; FCaptured := Capture; FFormatEtc := TFormatEtcList.Create(nil); // this creates a global cf_text by default with FFormatEtc.Add do FFormatEtc.Add; // this adds a global LCID code Format := cfLocale end;

The constructor remembers the string passed (in FCaptured) and creates a FormatEtcList that has two entries a cfText and a cfLocale (well why not). You see here the simplicity of using a TCollection to hold the format information. Note also that the call to "FFormatEtc.Add;", i.e. with no additional code, generates a cfText in global memory by default. The destructor just has to free the FFormatEtc instance.

I would emphasize that a copy of the edit contents is kept when the copy button is pressed. With clipboard operations it is possible for the user to select copy, then change the TEdit contents before pasting the data somewhere else. If we were to wait until the paste operation before getting the control's text, you would paste something different to what was there when copy was pressed. Not a good idea.

The DataObject's EnumFormatEtc method is now simply:

procedure TTestDataObject.EnumFormatEtc( Direction: integer; var EnumFormatEtc: IEnumFormatEtc; var Result : integer ); begin if Direction = ddGet then begin EnumFormatEtc := TStdEnumFormatEtc.Create(FFormatEtc); Result := ddOk end end;

Here I test to ensure that the caller is asking for a format enumerator (direction is "get" not "put"), and then return the IEnumFormatEtc interface that I described above. That gets the enumerator done. We have promised the availability of two formats (Text and Locale) both in global memory. When the call to GetData occurs we must hand over the data:

procedure TTestDataObject.GetData( const FormatEtc : TFormatEtc; var Medium : TStgMedium; var Result : integer ); begin Result := integer(ddBadFormatEtc); ZeroMemory (@Medium, sizeof (TStgMedium)); if FormatEtc.tymed = tsGlobal then begin Medium.tymed := tsGlobal; case FormatEtc.cfFormat of cfText : Medium.hGlobal := MakeGlobal (FCaptured); cfLocale : Medium.hGlobal := MakeGlobal else exit end; Result := ddOk end end;

This checks that the required medium is global memory, and then looks for the format asked for and if a cfText it returns a copy (another copy!!!) of the text in global memory (using the function MakeGlobal (String) in OleHelpers.pas) or if cfLocale it returns the LCID, again in global memory (MakeGlobal() for LCIDs is also in OleHelpers.pas).

And that just about completes the example. The bits left are that to cut the TEdit contents you copy it then clear the text, and to clear the clipboard you call OleSetClipboard(nil);.

Why all this hassle? Why not use Edit1.CopyToClipboard;? Well of course you can, but I'm illustrating a simple case of creating a text dataobject and passing it into the clipboard as a first step towards more complex items, and towards drag and drop.

Some Delphi Components

Next I want to present some non-visual components that are intended to help the creation of dataobjects by extracting information from properties of controls they're hooked up to. The basic idea, is that you have some control or other, you link it to a dataobject creator, and with the appropriate call out pops a dataobject containing a copy of the data from the control.

In OleDataObject.pas you'll find three components:

* TPictureDataSource - you can link this to a TImage or a TPicture and when asked for a DataObject it'll return one containing a TBitmap, TIcon, TMetafile (as an enhanced metafile), TGIFImage or TJPEGImage as appropriate, by means of events you can deal with other graphical types that are not predefined here. The component will also extract the palette and return a cfPalette, and also wrap the real image into a (old style) windows metafile as many applications can accept these images and not the others. DGDemo demonstrates the capabilities. Use another application (like DTView) to see the formats on the clipboard. If you don't want the overhead of GIFs or JPEGs then you can remove the two define statements in OLE.INC.

The JPEG image component I use is on the Delphi 5 CD in \Info\Extras\JPEG, and the GIF image I use, I found on the Torry web site as freeware (I hope!) and you'll find it in the Ole Library ZIP file. The Ole Drag and Drop components may need adjustments if different GIF or JPEG components are used.

* TStringDataSource - you can link this to any text that contains text, or multiple lines of text and it returns the single text items as cfText (or cfFilename or cfOemText if more appropriate), and returns multiple lines of text as cfCSV (comma separated values) (or cfPrinters, or cfFilenames if more appropriate). Where the text is extracted from depends on the control:

Notes:
1. TListBox ItemIndex property is not published
2. TPageControl Caption property obtained via ActivePage property
3. Called Tabs and TabIndex properties (pain)
4. Text property of the selected TTreeView node
5. Of the selected TListView nodes
6. Via the Selected property

The process I use is a mixture of using RTTI information and "hardwired" references to control properties. In general, the more simpler controls like Buttons and Edits you can get everything you need from RTTI, the more complex controls like ListView and TreeView you need to work a bit harder to get at the values. When you look through the code there is rather a lot to it, but it breaks down quite simply. DODemo demonstrates the capabilities by linking (by focus or clicking) onto a wide range of standard text controls. Use another application (like DTView) to see the formats on the clipboard. You can also provide the string directly into the Text property or a TStringList into the Lines property.

Finally...

* TComponentDataSource - this copies the status of the control using Delphi's in-built component streaming capabilities. It means that you can copy all of the published properties of a control (other than those with a default setting or are not stored) and paste the control as text or recreate the control in another application. DCDemo demonstrates the capability, it is admittedly only one application, but the copy and paste operations are made via the clipboard, not by any internal means.

The contents of OleDataSource.pas also include a general DataObject that is created and passed out on request. The three components described above are derived from an intermediate class (TDelpiDataSource) that contains two abstract methods that descendants must override in all cases.

Chapter 9 – Starting a Drag Source Operation ... Let's do it now!

Introduction

In the last chapter, I described the steps for a drag source operation as:

1. Decide that a drag source operation is required - this will involve some sort of button down testing
2. Provide an IDataObject object (DataObj)
3. Provide an IDropSource object (DropSource)
4. Describe what effects are permitted to be used (copy, move etc) (OkEffects)
5. The API returns whether there was an error, a drop or if the drop was cancelled (HResult)
6. On a successful drop, carryout the effect that was asked for (delete the original on move etc.) (Effect)

I then spent the rest of the chapter showing how step 2 operated for clipboard operations (and very little extra is needed for drag and drop). So in this chapter I'll look at the rest of these steps.

Another Data Source Component

But before I look at these steps I describe one other source data object. In the last chapter I described the TComponentDataSource that can be used to transfer a Delphi component in both a text form and a binary form thanks to built in Delphi functions. This can be used to transfer components between applications (including into the Delphi IDE if you try it!). But it does not replicate the functionality of the built in Delphi object drag and drop system. This transfers (a pointer to) the dragged object, giving you access to all of its properties and methods. But you can only drag such objects within the same Delphi application as its address space is only available to it there. It is a simple matter to have a data object that contains only a pointer, but such a pointer can only be valid within the source application. So how to test this? A variety of methods suggested themselves when I was pondering this, and the method given here is to pass a unique number along with the pointer, and any target application can test this number to see if it was the source (in which case the drop is valid). The number I've used is the ThreadId. So this is the data record:

type PDelphiObjectData = ^TDelphiObjectData; TDelphiObjectData = record Control : TObject; Thread : longword end;

The data has a clipboard format of cfObject (registered with windows in OleConsts.pas) and the class name is also passed as a cfText so the clipboard viewer will show at least that name as well as containing the binary data.

The target application must test the Thread value against its main thread value and only accept the data if these values are the same. Failure to observe this will result in rubbish data at best and an AV error at worst. Also if you pass this object to the clipboard you must remember that you are passing the pointer value and not a copy of the object itself. Therefore it is possible to 'copy' to the clipboard, modify the object and then a paste operation would fetch the modified object and not the object in the state that it was when copied. I hope this is clear as it is very important!

I think the process is robust and would welcome comment on it. A simple demo is provided, if you start two instances of the demo you'll see you can 'internally' drag and drop, but not between them. In this demo I also place all of the drag and drop non-visual controls on a TDataModule - this is probably the best way to stop the multitude of drag and drop components cluttering up the main form.

But before you can drag I need to tell you the rest!

3. Providing a DropSource object

The observant will note I've skipped step 1. This is because it's easier to look at the later steps first. An also the base component for drag source does not contain any drag start detection logic. OleInterface.pas contains the following wrapper class:

type TBaseDropSource = class (TInterfacedComponent, IDropSource) function QueryContinueDrag( fEscapePressed: BOOL; grfKeyState: Longint ): HResult; overload; stdcall; function GiveFeedback( dwEffect: Longint ): HResult; overload; stdcall; protected procedure QueryContinueDrag( EscapePressed : boolean; KeyState : TShiftState; var Result : integer ); overload; virtual; abstract; procedure GiveFeedback( Effect : integer; var Result : integer ); overload; virtual; abstract; end;

In the way that must now be quite familiar to you, it takes the C styled parameters and converts them in to more Delphi-friendly styles, and converts the key state information into a TShiftState set.

The new unit, OleDropSource.pas contains th TCustomDragSource class that overrides these abstract methods with some default behaviour.

QueryContinueDrag allows you to cancel, continue or accept that a drop has occured. The default behaviour ir to cancel if Esc has been pressed, accept a drop if the left mouse button has been released or otherwise continue:

procedure TCustomDragSource.QueryContinueDrag( EscapePressed : boolean; KeyState : TShiftState; var Result : integer ); begin if EscapePressed then // cancel the drop Result := ddCancel else if not (ssLeft in KeyState) then Result := ddDrop; // drop has occurred //... event code omitted end;

And secondly, GiveFeedback allows you to vary the cursor shape according to the drag effect (drop, move, link and scroll) that is underway. At a simplest level you can you can return a value of ddDefault in which case windows will select a standard cursor style. But I've added a bit of extra functionality allowing to to provide extra cursor shapes:

procedure TCustomDragSource.GiveFeedback (Effect : integer; var Result : integer); var Cursor : TCursor; begin Cursor := Cursors.Cursor (Effect); if Cursor = crDefault then Result := ddDefault else Result := ddOk; //... event code omitted if (Result = ddOk) and (Cursor <> crDefault) then SetCursor (Screen.Cursors [Cursor]) end;

 

The OleDragSource.pas unit also contains this class:

type TDropCursors = class (TPersistent) private //... omitted public function Cursor (Effect : integer) : TCursor; published property Copy : TCursor read FCopy write FCopy default crDefault; property CopyScroll : TCursor read FCopyScroll write FCopyScroll default crDefault; property Move : TCursor read FMove write FMove default crDefault; property MoveScroll : TCursor read FMoveScroll write FMoveScroll default crDefault; property Link : TCursor read FLink write FLink default crDefault; property LinkScroll : TCursor read FLinkScroll write FLinkScroll default crDefault; end;

This provides a place-holder for the six cursors possible together with a function that returns the TCursor value given the drag effect present. By default all of the cursors are set to crDefault which allows the standard windows cursors to be used.

The public part of TCustomDragSource is:

type TCustomDragSource = class (TBaseDropSource) private //... omitted protected //... omitted public constructor Create(AOwner : TComponent); override; destructor Destroy; override; function Execute : boolean; property Effect : integer read FEffect; property Copy : boolean read FCopy write FCopy default true; property Cursors : TDropCursors read FCursors write FCursors; property Link : boolean read FLink write FLink default false; property Move : boolean read FMove write FMove default false; property DataSource : TDelphiDataSource read FDataSource write FDataSource; property OnBeforeDrag : TBeforeDragEvent read FBeforeDrag write FBeforeDrag; property OnQueryDrag : TQueryDragEvent read FQueryDrag write FQueryDrag; property OnGiveFeedback : TGiveFeedbackEvent read FGiveFeedback write FGiveFeedback; property OnDragCancelled : TNotifyEvent read FDragCancelled write FDragCancelled; property OnAfterDrag : TAfterDragEvent read FAfterDrag write FAfterDrag; end;

The Copy, Move and Link properties are used to set if those effects are permitted, the DataSource is used to link to where the dragged data is coming from, there is a bunch of events that are mostly self explanatory and finally, the Execute function is used to set the drag operation running. As I said earlier this simple class contains no logic for deciding that a drag operation is required, I decided to split that out into a descendent component.

The Execute function deserves a little more attention:

function TCustomDragSource.Execute : boolean; var Effects : integer; Returned : HRESULT; DataObject : IDataObject; Cancel : boolean; begin Result := false; if not FDragging then begin // Assemble the Effects wanted Effects := deNone; if FCopy then Effects := Effects or deCopy; if FMove then Effects := Effects or deMove; if FLink then Effects := Effects or deLink; if (Effects <> deNone) and Assigned (FDataSource) then begin // Obtain the dataobject to drag DataObject := FDataSource.DataObject; try // Fire the OnBeforeDrag event passing // the dataobject, the effects // and allow the punter to cancel the drag Cancel := false; if Assigned (FBeforeDrag) then FBeforeDrag( Self, DataObject, Effects, Cancel ); if Cancel then Returned := ddCancel else begin // Call the API FDragging := true; try Returned := DoDragDrop( DataObject, Self, Effects, FEffect ) finally FDragging := false end end; // Deal with the results, returning // the function true if a drop occured, // False if a cancel occurred, raise // an exception on error (via OleCheck) case Returned of ddDrop : Result := true; ddCancel : ; // Result := false else OleCheck (Returned) end; // Fire the AfterDrag event to allow // the punter to take action if Assigned (FAfterDrag) then FAfterDrag (Self, FEffect, Result) finally // Release the DataObject DataObject := nil end end end end;

If you look through these comments you'll see the steps used are:

1. Assemble the effects permitted
    (4) Describe what effects are permitted to be used (copy, move etc))
2. Get the dataobject interface
3. Call the DoDragDrop API
    (5) The API returns whether there was an error, a drop or if the drop was cancelled)
4. Examine the returned values to determine if a drop took place and what effect was wanted
    (6) On a successful drop, carryout the effect that was asked for (delete the original on move etc.))
5. Release the dataobject interface

This component has a simple descendent, TStdDragSource that just publishes the public properties.

So this just leaves us with -

1. Deciding that a drag operation has started

The simplest way to do this is to test for the left button down and start a drag operation immediately. But this has disadvantages:

• What if the control accepts clicks or double clicks? Will these be lost?
• What if the control uses the mouse to highlight blocks of text? Can this be done?
• What if the punter accidentally clicks on a draggable control? Will we move or copy the object? How do we ignore such clicks i.e. how do we 'debounce' the mouse.

If you run WordPad or Word you can see how the mouse performs multiple functions - you can click to set the cursor position, you can click and swipe to mark a block of text and objects, you can click and drag a marked block. So we must ensure such functionality is present in our drag start operations.

The descendant component I am working towards encapsulates a variety of ways to decide if a drag operation has started. It then contains a property allowing you to select a method that best suits the control that you start the drag operation on.

1. Immediate - Look for a left button down and start a drag operation immediately - this is functionally very simple but I don't recommend it.

2. DragDetect - This calls the windows API DragDetect function that provides a simple debounce mechanism in that it waits for the mouse to move outside a small rectangle around the mouse down point (or Esc is pressed, or the left button is released) allowing you to decide if a drag operation must start. This is simple, works well but can be improved.

The next method relies on similar logic but also use a timer:

3. AllowDrag - When the left button is pressed you start a timer running, if the mouse moves outside the small rectangle and the mouse button is still down then the drag operation is started. So if the left button is released or the mouse is not moved a sufficient distance within a certain time then the drag operation is cancelled. - This works well most applications.

But none of these allow text in, say, a TMemo to be both dragged and selected by the mouse, so a modifiction to the AllowDrag logic is needed:

4. AllowSelect - When the left button is pressed you start a timer running, if the mouse is not moved outside the rectangle then a drag operation is started, if the mouse is moved then you can track its movement to select some text - this works well with a TMemo.

A complex example given later uses this logic to make a plain old TMemo drag and drop functional.

type TControlDragSource = class (TCustomDragSource) private //... omitted protected procedure Connect; virtual; procedure Disconnect; virtual; procedure Notification( AComponent: TComponent; Operation: TOperation ); override; procedure DebounceTimedOut(Sender : TObject); virtual; public constructor Create (AOwner : TComponent); override; destructor Destroy; override; property Pending : boolean read GetPending; property Timer : TTimer read FTimer; published property DragMode : TDragMode read FDragMode write SetDragMode default dmAutomatic; property Control : TControl read FControl write SetControl; property DebounceTime : integer read FDebounceTime write FDebounceTime default ddDragDelay; property DebounceDist : integer read FDebounceDist write FDebounceDist default ddDragMinDist; property DebounceMode : TDebounceMode read FDebounceMode write FDebounceMode default dmStartDrag; //... base properties published as well end;

This introduces the features needed to get this debouncing to work - the time, the distance and which of the debouncing methods to use. Also introduced is a TControl property that is the control that a drag operation can be started on. Note that this is a TControl, not a TWinControl which increases the flexibility. The source code is reasonably well documented and I suggest you look through it to see more of the logic explained.

TMemo DnD Extended

To demonstrate the ability to use the mouse to perform several functions I decided to apply drag and drop and Ole clipboard operations to TMemo. You'll find the complete code as a download. Writing the datasource and dragsource part was trivial, I just dropped the components onto the form, linked the components together using their properties and the memo's new functionality became operational. What took the time (and grief) was determining how drops can occur. There are 5 cases must be dealt with:

Internal drops - where some selected text is moved inside the same TMemo
1. Highlighted text is dropped on itself
2. Highlighted text is dropped earlier in the control
3. Highlighted text is dropped later in the control

And external drops - text from outside the TMemo is dropped
4. Dropped onto some already highlighted text occurs
5. Drop onto the memo control somewhere else occurs

We must also deal with Copy and Move operations and also allow dropping to the right of existing text (by filling in with spaces). Because this has little to do with drag and drop I won't elaborate further here. Undoubtedly the logic I've used can be improved but it does seem to work Ok. I suggest you trying dragging and dropping text between the demo application and WordPad to get a feel of how it works (I the slight differences between WordPad which is based on a RichEdit and this, simpler application). You can also try changing the de-bounce mode and control values to see how these influence the operation.

Chapter 10 – Worked Example 1 - Ole UI Dialogs and TOleContainer

Introduction

In the last chapter, I completed the methods needed to be a drag source and gave examples, in simple terms of its use. In the remaining few chapters in the series I am going to present a few fully working applications that incorporate COM based drag and drop, data-objects and consistent clipboard working. I also want to bring in an aspect entirely missing so far, which is linking, rather than just cutting and pasting.

To bring in linking to a source, of say, some text, requires rather more than just dropping the text onto a TEdit and expecting the control to link by itself. So to start with I'll look at a control that already knows how to link, that is a TOleContainer.

In approaching the demo using a TOleContainer, I became aware of how much code within OleCtrns.pas that was common to much of my OleXXXX.pas library (not surprisingly since some of it was pinched from there in the first place!). So my first step was to re-write and adapt much of TOleContainer into a new component called TOle2Container. Secondly, I have implemented the common Ole UI Dialogs, like the paste special dialog, as external components, so these would be provided externally and set up in the object inspector, rather than hard-wired into the TOle2Container code. Third, and last, I have used the new Ole Container in an MDI application that features copying, cutting, pasting, linking, all of the relevant Ole UI dialogs and not forgetting, dragging and dropping.

TOle2Container

The code for the new TOle2Container is in the download list below. The OleRegister unit contains the RegisterComponents call to install it on the component palette together with a component editor that does rather more than the TOleContainer editor. All of the Ole UI dialog calls can be made, but only those that are relevant to the container are shown in the right-click pop up menu (so for example, if the container object isn't linked, then the "update link" option will not be available).

The main changes to the component are:

• Any code common to other OleXXXX units already has been deleted
• The TOleForm class has uses elsewhere so has been placed in OleForm.pas
• The constants used have been changed to those in OleConsts.pas (mostly)
• All of the Ole UI dialogs are now linked using properties, only those actually linked become available, without the dialog the calls are ignored and the function returns false
• All of the Ole UI dialogs functionality has been implemented this therefore covers the following nine dialogs (only leaving the busy dialog which is not needed):
1. Paste special
2. Update links
3. Edit Links
4. Change Source
5. Change Icon
6. Insert object
7. Prompt user
8. Convert object
9. Object properties
• These dialogs have a couple of helper interfaces, implementations of these are imbedded in the implementation section of OleCtrns.pas. I have brought the interface into OleInterface.pas in the manner you will be aware I use if you have been reading this series, and put an implementation of the helper interfaces into OleLinks.pas. This implementation contains much of the common code that would be used in other places as well.
• Finally, I have made a number of methods public that were previously private or protected. Mainly because I think they are useful and are better off with higher visibility (IMHO).

A lot of the changes are cosmetic, moving code into other units to ease re-use in other projects, whilst others do improve the functionality of the component.

Ole UI Dialog Components

In previous chapters I showed how the Busy, Prompt and Paste Special dialogs in the Ole UI Library can be made functional as non-visual components on the component palette. This leaves seven other Ole UI Dialogs to cover so I'll be brief.

Insert Object Dialog

You are probably aware of this dialog as it turns up in other applications such as WordPad as well as being available in TOleContainer. It has several modes, such as create new or create from file, as well as being able to link to a file.

Convert Dialog

This enables one kind of inserted object to be converted to another, or activated as if another. The dialog is rare (is this a biology field trip or an chapter about Delphi?), not showing itself in either TOleContainer or WordPad. This dialog can be used to change the display to an icon.

Link Editor Dialog

This dialog provides a connection to many other Ole UI dialogs as well, from this dialog you can update a link, open the source file, change the source file and try and break the link to the source.

Change Source Dialog

This dialog allows you to change the source file that the container is linked to.

Change Icon Dialog

This was implemented in TOleContainer, when the object are displayed as an icon, rather than an image of the contents themselves, this dialog can be used to change the icon and the text label associated with the icon.


Object Properties Dialog

This dialog shows most (but not all) of the object's properties. The dialog will have two pages (General and View) for a contained object (non-linked) and the third page (Link), shown here, that describes the link.

Update Links Dialog

The last dialog is not one that you interact with, given a list of linked containers it updates the link associated with each of the, displaying the progress bar as it goes.

To use these dialogs, I suggest you look in Ole32.hlp for each of the dialogs to see what they do, and then use this demo application for an example of their implementation. There is a lot of code to go through, but if you want to implement Ole objects in an application then these dialogs are very useful. They provide consistency in appearance, and mean you don't have to code their functionality.

MDI Ole UI Dialog and Container Demo

This demo incorporates all of the changes outlined above into a new MDI application.

The main form contains the action list and images, all of the dialog components a menu bar and button bar:

And the child form contains the components to make drag and drop onto it work, by having a data source, drag source and drop target components. The child form also contains a TOle2Container, which is apparent from this screen dump, as I have put an avi file into it to make it visible.

There is, I admit, rather a lot of code to wade through, but hope you'll find my endeavours of some use! The topics I have looked at in this chapter, namely Ole UI dialogs, drag and drop and the extension of a familiar friend I will return to in my next chapter.

Chapter 11 – Worked Example 2 - Extending TRichEdit's Drag and Drop

Introduction

In the last chapter, I gave a range of extensions to the Ole Container to increase the capabilities of the component. In this chapter I shall give the TRichEdit component the same treatment.

If you have played with a TRichEdit you will know that it is text and rtf drag and drop aware, but if you drag and drop, say a bitmap, then the bitmap object is ignored by the Delphi component. But WordPad, which is based on the RichEdit control, does recognise dropped objects, and, for example, the Edit menu contains a Paste Special entry so the RichEdit should be capable of displaying these object so how do we do it? The answer, not suprisingly, is through COM interfaces.

A simple extension

Here's the final product - a TRichEdit holding a bitmap. I can get there in five minutes work and only 7 lines of code written by me:

What the TRichEdit requires of us is to provide the storage for object it handle the res t of the show. It accomplishes its request through an interface called IRichEditOleCallback (you'll find this defined in RichOle.pas), that can be implemented in Delphi by:

type TREOleCallBack = class (TInterfacedObject, IRichEditOleCallback) function GetNewStorage(out stg: IStorage): HRESULT; overload; stdcall; function GetInPlaceContext( out Frame: IOleInPlaceFrame; out Doc: IOleInPlaceUIWindow; var FrameInfo: TOleInPlaceFrameInfo ): HRESULT; overload; stdcall; function ShowContainerUI(fShow: BOOL): HRESULT; overload; stdcall; function QueryInsertObject( const clsid: TCLSID; stg: IStorage; cp: longint ): HRESULT; overload; stdcall; function DeleteObject(oleobj: IOLEObject): HRESULT; overload; stdcall; function QueryAcceptData( dataobj: IDataObject; var cfFormat: TClipFormat; reco: DWORD; fReally: BOOL; hMetaPict: HGLOBAL ): HRESULT; overload; stdcall; function ContextSensitiveHelp(fEnterMode: BOOL): HRESULT; overload; stdcall; function GetClipboardData( const chrg: TCharRange; reco: DWORD; out dataobj: IDataObject ): HRESULT; overload; stdcall; function GetDragDropEffect( fDrag: BOOL; grfKeyState: DWORD; var dwEffect: DWORD ): HRESULT; overload; stdcall; function GetContextMenu( seltype: Word; oleobj: IOleObject; const chrg: TCharRange; var menu: HMENU ): HRESULT; overload; stdcall; end;

This contains 10 functions of which only GetNewStorage function matters here (the rest can be made to return "ok" or "not implemented", as appropriate, to complete the interface). The enclosed demo uses two functions from my OleStd.pas library, one in the constructor of the interface to create the root storage and one in the GetNewStorage function to create a uniquely named sub-storage within the root.

When you have created an instance of the interface you register it with the TRichEdit by sending
an EM_SETOLECALLBACK message to the TRichEdit passing the address of the instance in the message. What then happens when you run the application is that when the TRichEdit component has an object dropped on it, it fires the GetNewStorage callback function and you allocate the requisite storage for the object. Then the object appears in the TRichEdit… bingo!

Full Extension

But you access to the object's functionality is limited, you can drop it in, and drag it back out, you can double click the object to activate it. The other functionality that you might want requires code written for the other functions of the IRichEditOleCallback interface. Additionally, you may notice that the call back interface is exactly that, the TRichEdit can ask you questions, but you cannot ask the TRichEdit questions though it in return. So to complete the conversation you must also ask the TRichEdit for an IRichEditOle interface (again in RichOle.pas) that enables you to ask questions or give instructions to the object aspects of the TRichEdit:

type IRichEditOle = interface ['{00020D00-0000-0000-C000-000000000046}'] function GetClientSite( out lplpolesite : IOLECLIENTSITE ) : HResult; stdcall; function GetObjectCount : longint; stdcall; function GetLinkCount : longint; stdcall; function GetObject( iob : longint; out reobject : TREObject; dwFlags : DWORD ) : HRESULT; stdcall; function InsertObject(const reobject : TREObject) : HResult; stdcall; function ConvertObject( iob : longint; const clsidNew : TCLSID; lpStrUserTypeNew : LPCSTR ) : HRESULT; stdcall; function ActivateAs( const clsid, clsidAs : TCLSID ) : HRESULT; stdcall; function SetHostNames( lpstrContainerApp, lpstrContainerObj : LPCSTR ) : HRESULT; stdcall; function SetLinkAvailable( iob : longint; fAvailable : BOOL ) : HRESULT; stdcall; function SetDvaspect( iob : longint; dvaspect : DWORD ) : HRESULT; stdcall; function HandsOffStorage(iob : longint) : HRESULT; stdcall; function SaveCompleted( iob : longint; stg : IStorage ) : HRESULT; stdcall; function InPlaceDeactivate : HRESULT; stdcall; function ContextSensitiveHelp(fEnterMode : BOOL) : HRESULT; stdcall; function GetClipboardData( const chrg : TCharRange; reco : DWORD; out dataobj : IDataObject ) : HRESULT; stdcall; function ImportDataObject( dataobj : IDataObject; cf : TClipFormat; hMetaPict : HGLOBAL) : HRESULT; stdcall; end;

So I have implemented this interface in a standard fashion in OleInterface.pas. The implementation takes care of registration with the TRichEdit (again using a SendMessage but this time receiving a pointer to the interface) and also translates the nasty C styled naming into something more Delphi friendly. The main implementation of the TRichEdit extension is in OleRE.pas; this unit contains the storage control outlined above, the code necessary for in-place activation (which it shares with Ole2Containers, which should be no surprise) and also all of the necessary code to link in the standard Ole UI dialogs described in the last chapter. In fact, the bulk of the code is about linking in these standard dialogs. And as you look through the code you'll see a remarkable similarity with Ole2Containers, which again should be no surprise.

To demonstrate the Ole extensions to the TRichEdit I have written a small MDI application that uses a TRichEdit on each child page, and the various Ole UI dialogs implemented by placing them on the main form together with all of the other bits and pieces needed to make an MDI application run. The application only really demonstrates the drag and drop and Ole UI extension of the TRichEdit, it does not provide the full extension of the RichEdit into such things as URL highlighting and paragraph formatting - I'm only interested here in a limited number of additions.


Main form:

Child Form:

Other Things

I have added a URL data source component, and also, added the generation of additional formats when dragging or doing clipboard operations. These new formats are: CF_FILEGROUPDESCRIPTOR and CF_FILECONTENTS which allow the dragged object to be dropped into explorer or onto the desktop. And also a simple implementation of CF_OBJECTDESCRIPTOR, this is normally used for embedding and linking but is also used to obtain the source of the information when carrying out a paste special, so it makes things appear much tidier than just "source: unknown" in the paste special Ole UI Dialog

So you'll find a new (and final) version of DTView and a simple URL example.

I will finish the series with chapter 12 with a final worked example that tries to bring together a few loose ends I've left dangling over the year that this series has taken to write.


Chapter 12 - Worked Example 3 - o Drag or not to Drag - That is the question

Last Chapter

This last chapter will be quite short. During the series I have commented in a number places that I would return to some idea or other "at a later date". Looking back, the most common comment was concerning mouse control. And, specifically, using the rodent to do several jobs. This last chapter, case study 3 in the series, examines how to detect or select what the punter is doing with the mouse. Specifically, is something being selected, or resized, or moved or dragged to be dropped onto the desktop or another application. Also the study will give an example of creating your own custom format for cut and paste, or drag and drop operations.

Blobby

The study itself uses a simple SDI application that uses a TShape to create a blob (or blobby from now on). The functions that I want to be able do to the blobby are:

1. Drag and drop into other instances of this application
2. Drag and drop into other apps a bitmap or metafile representation
3. File support - drag into desktop/explorer and back again
4. Drag and drop within this application to reposition the blobby
5. Automatic scrolling while dragging
6. Giving user feedback (an outline of the shape being dragged as a rectangle)
7. Clipboard cut and paste operations (both blobby directly and blobby files in explorer)
8. Drop a blobby save file onto the exe in explorer to launch the app
9. Using the mouse for other jobs as well such as resizing or moving the blobby

A common wish list?

Main Form

The main form, when looked at in the IDE is:

In brief, the application consists of a TPanel (the light gray area) that contains a TShape (the red square). Various buttons and menu items allows the colours, shape and edge thickness to be selected; menu items allow the blobby description to be saved to file, or loaded. There is an action list and image list and a variety of standard dialogs to bring the whole thing together. I'm not going to describe this stuff at all as it's pretty standard to any application, what I want to look at in detail is the use of the four drag and drop components.

DnDRun

When a file is dropped onto the executable in explorer (or wherever), then this component retrieves the filename and passes it (Item) to this event handler:

procedure TForm1.DnDRunDnDItem(Sender: TObject; const Item: String); begin if not FDoneDrop and (ExtractFileExt (UpperCase (Item)) = '.BLB') then begin SetBlobby (BlobbyDataFromFile (Item)); FDoneDrop := true end end;

It does little more than test the filename and then passes the filename to a procedure (SetBlobby) that obtains the blob design (colour, shape etc.). So much for this…

PictureDataSource

On the form, there is a hidden TImage, the purpose of this image is to hold a TGraphic formatted snapshot of the blob when a drag and drop operation starts. The TShape is not a graphic object in the sense that it cannot be linked to a picture data source. So the TImage is used as an intermediate store.

So when a drag operation starts, the blob is copied, via a TBitmap into the TImage:

// When the request for source data occurs, redraw the image into the invisible // TImage as a simple means of obtaining a bitmap and metafile procedure TForm1.PictureDataSourceBegin(Sender: TObject); var Bitmap : TBitmap; CRect : TRect; begin with TheShape do CRect := Rect (0, 0, Width, Height); Bitmap := TBitmap.Create; Bitmap.Width := TheShape.Width; Bitmap.Height := TheShape.Height; Bitmap.Canvas.CopyRect( CRect, TShapeCanvas(TheShape).Canvas, CRect ); CopyImage.Picture.Bitmap := Bitmap end;

The picture data source component knows how to render a cfBitmap and cfMetafilePict data formats for drag and drop operations. But, I want also to render a custom data format - the cfBlobby. So I need to add the format when requested:

// When a request for supported formats occurs add the cfBlobby format as well procedure TForm1.PictureDataSourceWantFormats(Sender: TObject; FormatList: TFormatEtcList); begin with FormatList.Add do Format := cfBlobby end;

And then supply the blobby data via a global handle. I also service a request for a cfFileDescriptor format here by returning a default blob filename and file size.

// When a request for data come in, look to see if it a cfBlobby in which case // pass the data back as a handle to global memory procedure TForm1.PictureDataSourceWantData( Sender: TObject; Format: Word; Medium: TClipMedium; Aspect: TClipAspect; AIndex: Integer; var Handle: Cardinal ); var B : TBlobbyRecord; T : TFileDescriptor; begin if (Format = cfBlobby) or (Format = cfFileContents)then begin B := GetBlobby; Handle := MakeGlobal (B, sizeof (TBlobbyRecord)) end; if Format = cfFileDescriptor then begin InitFileDescriptor (T); with T do begin FileName := 'Blobby.Blb'; FileSize := sizeof (TBlobbyRecord) end; Handle := MakeGlobal ([T]) end end;

Not forgetting that I must register my custom format:

initialization cfBlobby := RegisterClipboardFormat ('Blobby') end.

So that supplies the data, now what happens when a drag starts….

ControlDragSource

This is linked to the TShape as the control that can be dragged, and the PictureDataSource to supply the data for the drag. If you look at the code you will see that the events for this component just raise a flag before the drag starts, and then carries out some clean up after the operation has finished. Not rocket science.

The flag that is raised provides a shortcut in the operation that is worth exploring. When a drag operation starts, the punter could be either intending to copy or cut outside the application, or could just be moving the blob on the screen within this application. So by raising a flag as the drag operation starts, you can test it in the drop operation code and carryout a simpler move operation that mess around with COM and these components. This shortcut can be useful in many other applications and is worth bearing in mind.

ControlDropTarget

The final (well almost) bit is the detection and analysis of a dragged object, and the user feedback provided. When an object is dragged into the form (it "enters"), then this event is fired:

// When data is in dragged ---- procedure TForm1.ControlDropTarget1DragEnter( Sender: TObject; DataObject: IDataObject; State: TShiftState; MousePt: TPoint; var Effect, Result: Integer ); var Blob : TBlobbyRecord; begin Result := ddBad; //--- see if it contains a cfBlobby, // if so return Ok, or if it a *.blb filename with TEnumFormats.Create (DataObject) do try if HasFormat(cfBlobby) or (HasFormat (cfFilename) and (ExtractFileExt (UpperCase (Filename)) = '.BLB') ) then Result := ddOk finally Free end; // if ok then give feedback by showing a rectangle the size of the blob if (Result = ddOk) then begin if FLocalDrag and not FFirst then begin // Local and first call so obtain // the rectangle that fits over the existing // rectangle in screen coordinates FFirst := true; with TheShape do begin FRect.TopLeft := ClientToScreen(Point (0, 0)); FRect.BottomRight := ClientToScreen(Point(Width, Height)) end end else begin // Not local and first time so obtain the data object Blob := BlobbyFromDataObject(DataObject); FRect.TopLeft := MousePt; FRect.BottomRight := Point( MousePt.X + Blob.ShapeWidth, MousePt.Y + Blob.ShapeHeight ) end; // if we need a hDC then get one if FDC = 0 then FDC := GetDC (0); // xor the rectangle onto the screen DrawFocusRect (FDC, FRect); FMouseOrg := MousePt end end;

In turn this, looks to see if it contains a cfBlobby format, or is a recognised filename. If so, then it looks to see if the object is local (shortcuts…) in which case it obtains the rectangle position and size; if not local it obtains the data from the dataobject. Finally, using the rectangle, it calls the DrawFocusRect API. This xors the rectangle onto the screen; xor has the useful property that if a second xor operation is applied to the same rectangle then the screen is restored. Thus user feedback has started:

The rectangle shows the outside shape of the blobby being dragged and its current position. Most of the other events used by the control drop target component are about drawing moving and redrawing this rectangle as the mouse is moved around, or when the window is scrolled.

Finally, when a drop occurs, the data is retrieved and applied to the TShape:

// When a drop occurs..... procedure TForm1.ControlDropTarget1Drop( Sender: TObject; DataObject: IDataObject; State: TShiftState; MousePt: TPoint; var Effect, Result: Integer ); var NewPos : TPoint; Blob : TBlobbyRecord; begin //... erase the rectangle and release the DC ControlDropTarget1DragLeave (Self, Result); // if the drag was not local then get the data from the dataobject and set the // local blobby values to it if not FLocalDrag then begin Blob := BlobbyFromDataObject (DataObject); SetBlobby (Blob) end; // adjust the blobby coords to the new rectangle NewPos := ScrollBox.ScreenToClient (FRect.TopLeft); ShapePanel.Left := NewPos.X - ShapePanel.BorderWidth; ShapePanel.Top := NewPos.Y - ShapePanel.BorderWidth end;

If the blobby is just being moved (FLocalDrag is true) then very little needs to be done.

What about resizing?

The last function I want is to be able to resize the blobby. Here I have cheated a little. When I started on this application I was going to put "grab handles" on the corners and sides to enable resizing. In the end, I used a TPanel with a 6 pixel border, the TShape blobby is set to fill its client, so if you can resize the TPanel, the TShape resizes as well. So you will find a whole bunch of code associated with the TPanel that detects and gives user feedback (using DrawFocusRect) on resize operations.

Fine articolo originale

Considerazioni finali

Behh... siamo così arrivati alla fine, non mi sembra ci sia altro da aggiungere, rimane solo da analizzare i sorgenti, compilarsi gli esempi e divertirsi. Di seguito tutto il pacchetto compilabile sotto Delphi 7

DragNDrop.7z

 

 

 
 
Your Ad Here