Добавляем функционал Drag-n-Drop в приложение.
Давайте рассмотрим несколько разновидностей D&D:
- Самый простой вариант: перетаскивание данных из одного компонента в другой в пределах формы (переупорядочивание дерева TTreeView, перенос информации из дерева в TMemo, в TListView, перемещение вкладок TPageControl; вариантов очень много, все зависит только от вашей фантазии.
- Чуть сложнее: перетаскивание файлов/текста извне в наше приложение
- Еще чуть сложнее: перетаскивание файлов из нашего приложения наружу
Рассмотрим все три варианта:
1. Перетаскивание данных между компонентами, лежащими на форме, делается очень просто. Для примера - вот перенос из одного ListBox-а в другой:
выставляем для обоих ListBox-ов свойство DragMode в dmAutomatic, и назначаем события приемника (того компонента, куда будем бросать информацию) OnDragOver (для определения, стоит ли принимать то, что сейчас находится над компонентом, и может быть на него брошено. Если тут выставить параметр Accept в
false - то курсор мыши покажет, что бросать то, что принесли, бесполезно) и OnDragDrop (тут собственно предпринимаются действия по переносу данных из одного компонента в другой, то есть, из Source в Sender)
C++ |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| void __fastcall TForm1::ListBox2DragOver(TObject *Sender, TObject *Source, int X,
int Y, TDragState State, bool &Accept)
{
// принимать D&D только если перетягивается что-то из ListBox-а
Accept = (Source->ClassNameIs("TListBox") && static_cast<TListBox*>(Source)->ItemIndex > -1);
}
//---------------------------------------------------------------------------
void __fastcall TForm1::ListBox2DragDrop(TObject *Sender, TObject *Source, int X,
int Y)
{
TListBox *src = static_cast<TListBox*>(Source);
TListBox *targ = static_cast<TListBox*>(Sender);
// Все выделенные элементы в источнике, которых в приемнике
// еще нет, добавляем в приемник (дубликаты отсекаем)
for(int i = 0; i < src->Count; i++)
{
if(src->Selected[i] && targ->Items->IndexOf(src->Items->Strings[i]) < 0)
{
targ->Items->Add(src->Items->Strings[i]);
}
}
} |
|
По той же схеме делаются и другие переносы между компонентами на форме.
2. Перетаскивание данных (файлов или текста) в приложение из другого приложения:
Перетаскивание файлов и текста делается разными способами. Для того, чтобы форма умела принимать файлы, брошенные на нее извне (например, из Windows Explorer-а) достаточно добавить обработчик сообщения WM_DROPFILES:
C++ |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // заголовочный файл
class TForm1 : public TForm
{
__published: // IDE-managed Components
TListBox *ListBox1;
void __fastcall FormCreate(TObject *Sender);
void __fastcall FormDestroy(TObject *Sender);
private: // User declarations
public: // User declarations
__fastcall TForm1(TComponent* Owner);
protected:
void __fastcall WMDropFiles(TWMDropFiles &Message);
BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(WM_DROPFILES, TWMDropFiles, WMDropFiles);
END_MESSAGE_MAP(TForm);
}; |
|
C++ |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| // реализация
void __fastcall TForm1::WMDropFiles(TWMDropFiles &Message)
{
HDROP drop_handle = (HDROP)Message.Drop;
TCHAR fName[256];
// количество перетягиваемых файлов
int count = DragQueryFile(drop_handle, -1, NULL, NULL);
for (int i=0; i<count; i++)
{
// Получаем имя каждого файла
DragQueryFile(drop_handle, i, fName, 256);
// и обрабатываем его нужным образом (для примера - просто добавляем в ListBox)
ListBox1->Items->Add(fName);
};
DragFinish(drop_handle);
}
void __fastcall TForm1::FormCreate(TObject *Sender)
{
// уведомляем систему о том, что наша форма умеет обрабатывать WM_DROPFILES
DragAcceptFiles(this->Handle, true);
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
// ну, и по привычке, чистим за собой перед выходом
DragAcceptFiles(this->Handle, false);
} |
|
Чтобы иметь возможность перетянуть на форму текст нужно действовать по-другому. Для этого форма должна реализовать интерфейс
IDropTarget. Добавим в вышеприведенный код еще и такую возможность:
C++ |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| // Заголовочный файл
class TForm1 : public TForm, public IDropTarget
{
__published: // IDE-managed Components
TListBox *ListBox1;
TMemo *Memo1;
void __fastcall FormCreate(TObject *Sender);
void __fastcall FormDestroy(TObject *Sender);
private: // User declarations
public : // User declarations
__fastcall TForm1(TComponent* Owner);
protected:
void __fastcall WMDropFiles(TWMDropFiles &Message);
BEGIN_MESSAGE_MAP MESSAGE_HANDLER(WM_DROPFILES, TWMDropFiles, WMDropFiles);
END_MESSAGE_MAP(TForm);
// IUnknown
STDMETHOD_(ULONG, AddRef)()
{
return 1;
}
STDMETHOD_(ULONG, Release)()
{
return 1;
}
// IDropTarget
STDMETHOD(DragEnter)(LPDATAOBJECT pDataObj, DWORD grfKeyState, POINTL pt, LPDWORD pdwEffect);
STDMETHOD(DragOver)(DWORD grfKeyState, POINTL pt, LPDWORD pdwEffect);
STDMETHOD(DragLeave)();
STDMETHOD(Drop)(LPDATAOBJECT pDataObj, DWORD grfKeyState, POINTL pt, LPDWORD pdwEffect);
}; |
|
C++ |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
| // Реализация TForm1::WMDropFiles приведена выше, она не изменилась
void __fastcall TForm1::FormCreate(TObject *Sender)
{
OleInitialize(0);
// Регистрируем окно, как приемник Drag-n-Drop операций
OleCheck(RegisterDragDrop(this->Handle, this));
// уведомляем систему о том, что наша форма умеет обрабатывать еще и WM_DROPFILES
DragAcceptFiles(this->Handle, true);
}
// ---------------------------------------------------------------------------
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
// ну, и по привычке, чистим за собой перед выходом
DragAcceptFiles(this->Handle, false);
// Заодно прекращаем регистрацию приемника
RevokeDragDrop(this->Handle);
OleUninitialize();
}
// ---------------------------------------------------------------------------
// Реализация методов интерфейса IDropTarget
STDMETHODIMP TForm1::DragEnter(LPDATAOBJECT pDataObj, DWORD grfKeyState,
POINTL pt, LPDWORD pdwEffect)
{
*pdwEffect = DROPEFFECT_COPY;
return S_OK;
}
// ---------------------------------------------------------------------------
STDMETHODIMP TForm1::DragOver(DWORD grfKeyState, POINTL pt, LPDWORD pdwEffect)
{
*pdwEffect = DROPEFFECT_COPY;
return S_OK;
}
// ---------------------------------------------------------------------------
STDMETHODIMP TForm1::DragLeave()
{
return S_OK;
}
// ---------------------------------------------------------------------------
// Основной метод - при отпускании объекта на форме вызывается именно он
STDMETHODIMP TForm1::Drop(LPDATAOBJECT pDataObj, DWORD grfKeyState, POINTL pt,
LPDWORD pdwEffect)
{
FORMATETC fmtetc;
fmtetc.cfFormat = CF_UNICODETEXT; // или просто CF_TEXT, если собирается неюникодный проект
fmtetc.ptd = NULL;
fmtetc.dwAspect = DVASPECT_CONTENT;
fmtetc.lindex = -1;
fmtetc.tymed = TYMED_HGLOBAL;
STGMEDIUM medium;
// получаем данные из перетягиваемого объекта
HRESULT hr = pDataObj->GetData(&fmtetc, &medium);
// если при этом не было ошибки
if (!FAILED(hr))
{
HGLOBAL hTxt = medium.hGlobal;
// то вытягиваем текст, который перетянули на форму
LPCTSTR pData = (LPCTSTR)GlobalLock(hTxt);
// и заносим его в Memo
Memo1->Text = pData;
// после чего разблокируем память
GlobalUnlock(hTxt);
// и освобождаем Storage medium
ReleaseStgMedium(&medium);
}
else // если была ошибка при получении данных из IDataObject
{
*pdwEffect = DROPEFFECT_NONE;
// то ничего не делаем и возвращаем код этой ошибки
return hr;
}
return S_OK;
} |
|
Таким образом, наша форма сейчас умеет принимать и файлы и текст, если на нее бросить текст из Notepad++ или из Firefox-а (т.е., из тех приложений, которые
умеют быть источниками при перетаскивании текста, обычный виндовый Блокнот, к примеру, не умеет этого, также как и браузер Opera), то этот текст появится в Memo, а если бросить группу файлов из Проводника - то их имена добавятся в ListBox.
3. Вот мы и добрались до пункта, когда нужно из нашего приложения отослать текст или файл наружу.
Для этого нужно реализовать интерфейс
IDropSource, перенести требуемый текст в
IDataObject, и вызвать функцию
DoDragDrop
Основной код в этом случае будет выглядеть так (обработка сообщений через WinAPI понадобилась исключительно для того, чтобы при перемещении выделенного участка текста, выделение с него не снималось) :
C++ |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| case WM_MOUSEMOVE: // в случае, если пользователь перемещает мышь над Memo
// и кнопка мыши нажата
if (fMouseDown)
{
IDataObject *pDataObject;
IDropSource *pDropSource;
DWORD dwEffect;
DWORD dwResult;
FORMATETC fmtetc =
{CF_UNICODETEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL}; // CF_TEXT для BCB 6 - 2007
STGMEDIUM stgmed =
{TYMED_HGLOBAL, {0}, 0};
// Перемещаем выделенный в Memo текст в IDataObject
stgmed.hGlobal = CopySelection();
// Создаем экземпляры COM-объектов IDataObject и IDropSource
CreateDropSource(&pDropSource);
CreateDataObject(&fmtetc, &stgmed, 1, &pDataObject);
// Начинаем операцию Drag-n-Drop
dwResult = DoDragDrop(pDataObject, pDropSource,
DROPEFFECT_COPY | DROPEFFECT_MOVE, &dwEffect);
// В случае успеха
if (dwResult == DRAGDROP_S_DROP)
{
if (dwEffect & DROPEFFECT_MOVE)
{
// Если текст был именно перенесен, а не скопирован
// (при выполнении Drag-n-Drop была зажата кнопка Alt,
// а не Ctrl) - удалить выделенный текст из Memo
this->SelText = "";
}
}
// операция переноса отменена
else if (dwResult == DRAGDROP_S_CANCEL)
{
}
pDataObject->Release(); // и удаляем все объекты
pDropSource->Release();
ReleaseCapture();
fMouseDown = FALSE;
fDidDragDrop = TRUE;
}
break; |
|
Полностью работоспособный пример, реализующий перетаскивание с формы файлов и текстовой информации присоединен в архиве:
Я привел проекты, показывающие реализацию всех трех разновидностей Drag-n-Drop для разных версий Билдера (для BCB6 и для RAD XE2). В принципе они одинаковые, но есть несколько отличий:
- При работе с Юникодными версиями Билдера поле cfFormat структуры FORMATETC должно содержать значение CF_UNICODETEXT, в то время как при использовании неюникодных Билдеров туда пишется CF_TEXT
- Начиная с версии RAD 2011 нет необходимости переопределять методы IUnknown (_AddRef и _Release) вручную, достаточно унаследоваться от TInterfacedObject и воспользоваться реализациями TInterfacedObject::_AddRef() и TInterfacedObject::_Release().
(хотя у меня в BCB6 Portable прекрасно поддерживаются TInterfacedObject-ы, поэтому переделывать пока не стал, оставил как есть)
P.S. Замечания/пожелания/вопросы приветствуются, обращайтесь в ЛС.
P.P.S. Продолжение следует (реализация Drag-n-Dock в приложениях)