# 11.3 實現拖放操作
你可以在你的應用程序中實現拖放源,拖放目標或者兩者同時實現.
實現拖放源
要實現一個拖放源,也就是說要提供用戶用于拖放操作的數據,你需要使用一個wxDropSource類的實例.要注意下面描述的事情都是在你的應用程序已經認定一個拖放操作已經開始以后發生的.決定拖放是否開始的邏輯,是完全需要由應用程序自己決定的,一些控件會通過產生一個拖放開始事件來通知應用程序, 在這種情況下,你不需要自己關心這一部分的邏輯(對這一部分的邏輯的關心,可能會讓你的鼠標事件處理代碼變得混亂).在本章中,也會提供一個何時 wxWidgets通知你拖放操作開始的大概描述.
一個拖放源需要采取的動作包括下面幾步:
1 準備工作
首先,必須先創建和初始化一個將被拖動的數據對象,如下所示:
```
wxTextDataObject myData(wxT("This text will be dragged."));
```
2 開始拖動要開始拖動操作,最典型的方式是響應一個鼠標單擊事件,創建一個wxDropSource對象,然后調用它的wxDropSource::DoDragDrop函數,如下所示:
```
wxDropSource dragSource(this);
dragSource.SetData(myData);
wxDragResult result = dragSource.DoDragDrop(wxDrag_AllowMove);
```
下表列出的標記,可以作為DoDragDrop函數的參數:
| wxDrag_CopyOnly | 只允許進行拷貝操作. |
|:--- |:--- |
| wxDrag_AllowMove | 允許進行移動操作. |
| wxDrag_DefaultMove | 默認操作是移動數據. |
當創建wxDropSource對象的時候,你還可以指定發起拖動操作的窗口,并且可以選擇拖動使用的光標,可選的范圍包括拷貝,移動以及不能釋放等.這些光標在GTK+上是圖標,而在別的平臺上是光標,因此你需要使用wxDROP_ICON來屏蔽這種區別,正如我們很快就將看到的拖動文本的例子中演示的那樣.
3 拖動
對DoDragDrop函數的調用將會阻止應用程序進行其他處理,直到用戶釋放鼠標按鈕(除非你重載了GiveFeedback函數以便進行其他的特殊操作)當鼠標在應用程序的一個窗口上移動時,如果這個窗口可以識別這個拖動操作協議,對應的wxDropTarget函數就會被調用,參考接下來的小節"實現一個拖放目的"
4 處理拖放結果
DoDragDrop函數返回一個拖放操作的結果,這個返回值的類型為wxDragResult,它的枚舉值如下表所示:
| wxDragError | 在拖動操作的執行過程中出現了錯誤. |
|:--- |:--- |
| wxDragNone | 數據源不被拖動目的接受. |
| wxDragCopy | 數據已經被成功拷貝. |
| wxDragMove | 數據已經被成功移動(僅適用于Windows). |
| wxDragLink | 以完全一個鏈接操作. |
| wxDragCancel | 用戶已經取消了拖放操作. |
你的應用程序可以針對不同的返回值進行自己的操作,如果返回值是wxDragMove,通常你需要刪除綁定在數據源中的數據,然后更新屏幕顯示.而如果返回值是wxDragNone,則表示拖動操作已經被取消了.下面舉例說明:
```
switch (result)
{
case wxDragCopy: /* 數據被拷貝或者被鏈接:
無需特別操作 */
case wxDragLink:
break;
case wxDragMove: /* 數據被移動,刪除原始數據 */
DeleteMyDraggedData();
break;
default: /* 操作被取消或者數據不被接受
或者發生了錯誤:
不做任何操作 */
break;
}
```
下面的例子演示了怎樣實現一個文本數據拖放源.DnDWindow包含一個m_strText成員變量,當鼠標左鍵按下的時候,針對 m_strText的拖放操作開始,拖放操作的結果通過一個消息框顯示.另外,拖放操作將會在鼠標已經拖動了一小段距離后才會開始,因此單擊鼠標動作并不會導致一個拖放操作.
```
void DnDWindow::OnLeftDown(wxMouseEvent& event )
{
if ( !m_strText.IsEmpty() )
{
// 開始拖動操作
wxTextDataObject textData(m_strText);
wxDropSource source(textData, this,
wxDROP_ICON(dnd_copy),
wxDROP_ICON(dnd_move),
wxDROP_ICON(dnd_none));
int flags = 0;
if ( m_moveByDefault )
flags |= wxDrag_DefaultMove;
else if ( m_moveAllow )
flags |= wxDrag_AllowMove;
wxDragResult result = source.DoDragDrop(flags);
const wxChar *pc;
switch ( result )
{
case wxDragError: pc = wxT("Error!"); break;
case wxDragNone: pc = wxT("Nothing"); break;
case wxDragCopy: pc = wxT("Copied"); break;
case wxDragMove: pc = wxT("Moved"); break;
case wxDragCancel: pc = wxT("Cancelled"); break;
default: pc = wxT("Huh?"); break;
}
wxMessageBox(wxString(wxT("Drag result: ")) + pc);
}
}
```
實現一個拖放目的
要實現一個拖放目的,也就是說要接收用戶拖動的數據,你需要使用wxWindow::SetDropTarget函數,將某個窗口和一個 wxDropTarget綁定在一起,你需要實現一個wxDropTarget的派生類,并且重載它的所有純虛函數.另外還需要重載OnDragOver 函數,以便返回一個wxDragResult類型的返回碼,以說明當鼠標指針移過這個窗口的時候,光標應該怎樣顯示,并且重載OnData函數來實現放置操作.你還可以通過繼承wxTexTDropTarget或者wxFileDropTarget,或者重載它們的OnDropText或者 OnDropFiles函數來實現一個拖放目的.
下面的步驟將發生在拖放操作過程當中的拖放目的對象上.
1 初始化
wxWindow::SetDropTarget函數在窗口創建期間被調用,以便將其和一個拖放目的對象綁定.在窗口創建或者應用程序的其他某個部分,通過函數wxDropTarget::SetDataObject,拖放目的對象和某一種數據類型綁定,這種數據類型將用來作為拖放源和播放目的進行協商的依據.
2 拖動
當鼠標在拖放目的上以拖動的方式移動時,wxDropTarget::OnEnter,wxDropTarget:: OnDragOver和 wxDropTarget::OnLeave函數將在適當的時候被調用,它們都將返回一個對應的wxDragResult值.以便拖放操作可以對其進行合適的用戶界面反饋.
3 放置
當用戶釋放鼠標按鈕的時候,wxWidgets通過調用函數wxDataObject::GetAllFormats詢問窗口綁定的 wxDropTarget對象是否接受正在拖動的數據.如果數據類型是可接受的,那么wxDropTarget::OnData將被調用.拖放對象綁定的 wxDataObject對象將進行對應的數據填充動作.wxDropTarget::OnData函數將返回一個wxDragResult類型的值,這個值將作為wxDropSource::DoDragDrop函數的返回值.
使用標準的拖放目的對象
wxWidgets提供了了標準的wxDropTarget的派生類,因此你不必在任何時候都需要實現自己的拖放對象.你只需要實現重載這些類的一個虛函數,以便在拖放的時候得到提示.
wxTextdropTarget對象可以接收被拖動的文本數據,你只需要重載OnDropText函數以便告訴wxWidgets當有文本數據被放置的時候做什么事情就可以了.下面的例子演示了當有文本數據被放置的時候,怎樣將其添加列表框內.
```
// 一個拖放目的用來將文本填加到列表框
class DnDText : public wxTextDropTarget
{
public:
DnDText(wxListBox *owner) { m_owner = owner; }
virtual bool OnDropText(wxCoord x, wxCoord y,
const wxString& text)
{
m_owner->Append(text);
return true;
}
private:
wxListBox *m_owner;
};
// 設置拖放目的對象
wxListBox* listBox = new wxListBox(parent, wxID_ANY);
listBox->SetDropTarget(new DnDText(listBox));
```
下面的例子展示了怎樣使用wxFileDropTarget,這個對象可接收從資源管理器里拖動的文件對象,并且報告拖動文件的數目以及它們的名稱.
```
// 一個拖放目的類用來將拖動的文件名添加到列表框
class DnDFile : public wxFileDropTarget
{
public:
DnDFile(wxListBox *owner) { m_owner = owner; }
virtual bool OnDropFiles(wxCoord x, wxCoord y,
const wxArrayString& filenames)
{
size_t nFiles = filenames.GetCount();
wxString str;
str.Printf( wxT("%d files dropped"), (int) nFiles);
m_owner->Append(str);
for ( size_t n = 0; n < nFiles; n++ ) {
m_owner->Append(filenames[n]);
}
return true;
}
private:
wxListBox *m_owner;
};
// 設置拖放目的類
wxListBox* listBox = new wxListBox(parent, wxID_ANY);
listBox->SetDropTarget(new DnDFile(listBox));
```
創建一個自定義的拖放目的
現在我們來創建一個自定義的拖放目的,它可以接受URLs(網址).這一次我們需要重載OnData和 OnDragOver函數,我們還將實現一個可以被重載的虛函數OnDropURL.
```
// 一個自定義的拖放目的對象,可以拖放URL對象
class URLDropTarget : public wxDropTarget
{
public:
URLDropTarget() { SetDataObject(new wxURLDataObject); }
void OnDropURL(wxCoord x, wxCoord y, const wxString& text)
{
// 當然, 一個真正的應用程序在這里應該做些更有意義的事情
wxMessageBox(text, wxT("URLDropTarget: got URL"),
wxICON_INFORMATION | wxOK);
}
// URLs不能被移動,只能被拷貝
virtual wxDragResult OnDragOver(wxCoord x, wxCoord y,
wxDragResult def)
{
return wxDragLink;
}
// 這個函數調用了OnDropURL函數,以便它的派生類可以更方便的使用
virtual wxDragResult OnData(wxCoord x, wxCoord y,
wxDragResult def)
{
if ( !GetData() )
return wxDragNone;
OnDropURL(x, y, ((wxURLDataObject *)m_dataObject)->GetURL());
return def;
}
};
// 設置拖放目的對象
wxListBox* listBox = new wxListBox(parent, wxID_ANY);
listBox->SetDropTarget(new URLDropTarget);
```
更多關于wxDataObject的知識
正如我們已經看到的那樣,wxDataObject用來表示所有可以被拖放.以及可以被剪貼板操作的數據.wxDataObject最重要的特性之一,是在于它是一個"聰明"的數據塊,和普通的包含一段內存緩沖,或者一些文件的啞數據不同.所謂"聰明"指的是數據對像自己可以知道它內部的數據可以支持什么數據格式,以及怎樣將它的內部數據表現為那種數據格式.
所謂支持的數據格式,意思是說,這種格式可以從一個數據對象的內部數據或者將被設置的內部數據產生.通常情況下,一個數據對象可以支持多種數據格式作為輸入或者輸出.因此,一個數據對象可以支持從一種格式創建內部數據,并將其轉換為另外一種數據格式,反之亦然.
當你需要使用某種數據對象的時候,有下面幾種可選方案:
1. 使用一種內建的數據對象.當你只需要支持文本數據,圖片數據,或者是文件列表數據的時候,你可以使用wxTextdataObject,wxBitmapDataObject,或wxFileDataObject.
2. 使用wxDataObjectSimple.從wxDataObjectSimple產生一個派生類,是實現自定義數據格式的最簡便的方法,不過,這種派生類只能支持一種數據格式,因此,你將不能夠使用它和別的應用程序進行交互.不過,你可以用它在你的應用程序以及你應用程序的不同拷貝之間進行數據傳輸.
3. 使用wxCustomDataObject的派生類(它是wxDataObjectSimple的一個子類)來實現自定義數據對象.
4. 使用wxDataObjectComposite.這是一個簡單而強大的解決方案,它允許你支持任意多種數據格式(如果你和前面的方法結合使用的話,可以實現同時支持標準數據和自定義數據).
5. 直接使用wxDataObject.這種方案可以實現最大的靈活度和效率,但也是最難的一種實現方案.
在拖放操作和剪貼板操作中,使用多重數據格式最簡單的方法是使用wxDataObjectComposite對象,但是,這種使用方法的效率是很低的,因為每一個wxDataObjectSimple都使用自己定義的格式來保存所有的數據.試想一下,你將從剪貼板上以你自己的格式粘貼超過兩百頁的文本,其中包含Word,RTF,HTML,Unicode,和普通文本,雖然現在計算機的能力已經足可以應付這樣的任務,但是從性能方面考慮, 你最好還是直接從wxDataObject實現一個派生類,用它來定義你的數據格式,然后在程序中指定這種類型的數據.
剪貼板操作和拖放操作,潛在的數據傳輸機制將在某個應用程序真正請求數據的時候,才會進行數據拷貝.因此,盡管用戶可能認為在自己點擊了應用程序的拷貝按鈕以后數據就已存在于剪貼板了,但實際上這時候僅僅是告訴剪貼板有數據存在了.
實現wxDataObject的派生類
我們來看一下實現一個新的wxDataObject的派生類要用到哪些東西.至于怎樣實現的過程,我們前面已經介紹過了,它是非常簡單的.因此,我們在這里不多說了.
每一個wxDataObject的派生類,都必須重載和實現它的純虛成員函數,那些只能用來輸出數據或者保存數據(意味著只能進行單向操作)的數據對象的GetFormatCount函數在其不支持的方向上應該總是返回零.
GetAllFormats函數的參考為一個wxDataFormat類型的列表,以及一個方向(獲取或設置).它將所有自己在這個方向上支持的數據格式填入這個列表.GetFormatCount函數則用來檢測列表中元素的個數.
GetdataHere函數的參數是一個wxDataFormat參數,以及一個void*緩沖區.如果操作成功,返回TRue,否則返回false.這個函數必須將數據以給定的格式填入這個緩沖區,數據可以是任意的二進制數據,或者是文本數據,只要SetData函數可以識別就行了.
GetdataSize函數則返回在某種給定的數據格式下數據的大小.
GetFormatCount函數返回用于轉換或者設置的當前支持數據類型的個數.
GetPreferredFormat函數則返回某個指定方向上優選的數據類型.
SetData函數的參考包括一個數據類型,一個整數格式的緩沖區大小,以及一個void*類型的緩沖區指針.你可以在適當的時候(比如將其拷貝到自己的內部結構的時候)對其進行適當的解釋.這個函數在成功的時候返回TRue,失敗的時候返回false.
wxWidgets的拖放操作例子
我們通過使用位于samples/dnd目錄的wxWidgets的拖放操作的例子,來演示一下怎樣制作一個自定義的擁有自定義數據類型的數據對象.這個例子演示了一個簡單的繪圖軟件,可以用來繪制矩形,三角形,或者橢圓形,并且允許你對其進行編輯,拖放到一個新的位置,拷貝到剪貼板以及從新粘貼到應用程序等操作.你可以通過選擇文件菜單中的新建命令來創建一個應用程序的主窗口.這個窗口的外觀如下圖所示:

這些圖形是用繼承字DnDShape的類來建模的,數據對象被稱為DnDShapeDataObject.在學習怎樣實現這個數據對象之前,我們先看一下應用程序是怎樣使用它們的.
當一個剪貼板拷貝操作被請求的時候,一個DnDShapeDataObject對象將被增加到剪貼板,這個對象包含當前正在操作的圖形的拷貝,如果剪貼板上已經有了一個對象,那么舊的對象將被釋放.代碼如下所示:
```
void DnDShapeFrame::OnCopyShape(wxCommandEvent& event)
{
if ( m_shape )
{
wxClipboardLocker clipLocker;
if ( !clipLocker )
{
wxLogError(wxT("Can't open the clipboard"));
return;
}
wxTheClipboard->AddData(new DnDShapeDataObject(m_shape));
}
}
```
剪貼板的粘貼操作也很容易理解,調用wxClipboard::GetData函數來獲取位于剪貼板的圖形數據對象,然后從其中獲取圖形數據.前面我們已經介紹過怎樣在剪貼板擁有對應數據的時候允許paste菜單.shapeFormatId是一個全局變量,其中包含了自定義的數據格式名稱:wxShape.
```
void DnDShapeFrame::OnPasteShape(wxCommandEvent& event)
{
wxClipboardLocker clipLocker;
if ( !clipLocker )
{
wxLogError(wxT("Can't open the clipboard"));
return;
}
DnDShapeDataObject shapeDataObject(NULL);
if ( wxTheClipboard->GetData(shapeDataObject) )
{
SetShape(shapeDataObject.GetShape());
}
else
{
wxLogStatus(wxT("No shape on the clipboard"));
}
}
void DnDShapeFrame::OnUpdateUIPaste(wxUpdateUIEvent& event)
{
event.Enable( wxTheClipboard->
IsSupported(wxDataFormat(shapeFormatId)) );
}
```
為了實現拖放操作,還需要一個拖放目標對象,以便在圖片數據被放置的時候通知應用程序.DnDShapeDropTarget類包含一個 DnDShapeDataObject數據對象,用來扮演這個角色,當它的OnData函數被調用的時候,則表明正在放置一個圖形數據對象,下面的代碼演示了DnDShapeDropTarget的聲明及實現:
```
class DnDShapeDropTarget : public wxDropTarget
{
public:
DnDShapeDropTarget(DnDShapeFrame *frame)
: wxDropTarget(new DnDShapeDataObject)
{
m_frame = frame;
}
// 重載基類的(純)虛函數
virtual wxDragResult OnEnter(wxCoord x, wxCoord y, wxDragResult def)
{
m_frame->SetStatusText(_T("Mouse entered the frame"));
return OnDragOver(x, y, def);
}
virtual void OnLeave()
{
m_frame->SetStatusText(_T("Mouse left the frame"));
}
virtual wxDragResult OnData(wxCoord x, wxCoord y, wxDragResult def)
{
if ( !GetData() )
{
wxLogError(wxT("Failed to get drag and drop data"));
return wxDragNone;
}
// 通知主窗口正在進行放置
m_frame->OnDrop(x, y,
((DnDShapeDataObject *)GetDataObject())->GetShape());
return def;
}
private:
DnDShapeFrame *m_frame;
};
```
在應用程序初始化函數里,主窗口被創建的時候設置這個拖放目標:
```
DnDShapeFrame::DnDShapeFrame(wxFrame *parent)
: wxFrame(parent, wxID_ANY, _T("Shape Frame"))
{
...
SetDropTarget(new DnDShapeDropTarget(this));
...
}
```
當鼠標左鍵單擊的時候,拖放操作開始,其處理函數將創建一個wxDropSource對象,并且給DoDragDrop 函數傳遞一個DnDShapeDataObject對象,以便初始化拖放操作.DndShapeFrame::OnDrag函數如下所示:
```
void DnDShapeFrame::OnDrag(wxMouseEvent& event)
{
if ( !m_shape )
{
event.Skip();
return;
}
// 開始拖放操作
DnDShapeDataObject shapeData(m_shape);
wxDropSource source(shapeData, this);
const wxChar *pc = NULL;
switch ( source.DoDragDrop(true) )
{
default:
case wxDragError:
wxLogError(wxT("An error occured during drag and drop"));
break;
case wxDragNone:
SetStatusText(_T("Nothing happened"));
break;
case wxDragCopy:
pc = _T("copied");
break;
case wxDragMove:
pc = _T("moved");
if ( ms_lastDropTarget != this )
{
// 如果這個圖形被放置在自己的窗口上
// 不要刪除它
SetShape(NULL);
}
break;
case wxDragCancel:
SetStatusText(_T("Drag and drop operation cancelled"));
break;
}
if ( pc )
{
SetStatusText(wxString(_T("Shape successfully ")) + pc);
}
//在其他情況下,狀態文本已經被設置了
}
```
當用戶釋放鼠標以表明正在執行放置操作的時候,wxWidgets調用DnDShapeDropTarget::OnData函數,這個函數將以一個新的DndShape對象來調用DndShapeFrame::OnDrop函數,以便給DndShape對象設置一個新的位置.這樣,拖放操作就完成了.
```
void DnDShapeFrame::OnDrop(wxCoord x, wxCoord y, DnDShape *shape)
{
ms_lastDropTarget = this;
wxPoint pt(x, y);
wxString s;
s.Printf(wxT("Shape dropped at (%d, %d)"), pt.x, pt.y);
SetStatusText(s);
shape->Move(pt);
SetShape(shape);
}
```
現在,唯一剩下的事情,就是實現自定義的wxDataObject對象了.為了說明得更清楚,我們將對整個實現進行逐項說明.首先,我們來看一下自定義數據類型標識符聲明,以及DndShapeDataObject類的聲明,它的構造函數和析構函數,和它的數據成員.
數據類型標識符是shapeFormatId,它是一個全局變量,在整個例子中都有使用.構造函數通過GeTDataHere函數獲得當前圖形(如果有的話)的一個拷貝作為參數.這個拷貝也可以通過DndShape::Clone函數產生.DnDShapeDataObject的析構函數將會釋放這個拷貝.
DndShapeDataObject可以提供位圖和(在支持的平臺上)源文件來表示它的內部數據.因此,它還擁有 wxBitmapDataObject和wxMetaFileDataObject兩個類型的數據成員(以及一個標記用來指示當前正在使用哪種類型)來緩存內部數據以便在需要的時候提供這種格式.
```
// 自定義的數據格式標識符
static const wxChar *shapeFormatId = wxT("wxShape");
class DnDShapeDataObject : public wxDataObject
{
public:
// 構造函數沒有直接拷貝指針
// 這樣在原來的圖形對象被釋放以后,這里的圖形對象是有效的
DnDShapeDataObject(DnDShape *shape = (DnDShape *)NULL)
{
if ( shape )
{
// 我們需要拷貝真正的圖形對象,而不是只拷貝指針
// 這是因為圖形對象有可能在任何時候被刪除,在這種情況下
// 剪貼板上的數據仍然應該是有效的
// 因此我們使用下邊的方法來實現圖形拷貝
void *buf = malloc(shape->DnDShape::GetDataSize());
shape->GetDataHere(buf);
m_shape = DnDShape::New(buf);
free(buf);
}
else
{
// 不需要拷貝任何東西
m_shape = NULL;
}
// 這個字符串應該用來唯一標識我們的數據格式類型
// 除此以外,它可以是任意的字符串
m_formatShape.SetId(shapeFormatId);
// 我們直到需要的(也就是數據被第一次請求)時候才產生圖片或者元文件數據
m_hasBitmap = false;
m_hasMetaFile = false;
}
virtual ~DnDShapeDataObject() { delete m_shape; }
// 在這個函數被調用以后,圖形數據歸調用者所有
// 調用者將負責釋放相關內存
DnDShape *GetShape()
{
DnDShape *shape = m_shape;
m_shape = (DnDShape *)NULL;
m_hasBitmap = false;
m_hasMetaFile = false;
return shape;
}
// 其他成員函數省略
...
// 數據成員
private:
wxDataFormat m_formatShape; // 我們的自定義格式
wxBitmapDataObject m_dobjBitmap; // 用來響應位圖格式請求
bool m_hasBitmap; // 如果m_dobjBitmap有效為真
wxMetaFileDataObject m_dobjMetaFile;// 用來響應元數據格式請求
bool m_hasMetaFile;// 如果m_dobjMetaFile有效為真
DnDShape *m_shape; // 原始數據
};
```
接下來我們來看一下那些用于回答和我們內部存儲的數據相關的問題的函數.GetPreferredFormat只簡單的返回 m_formatShape數據綁定的本地的數據格式,它是在我們的構造函數中使用wxShape類型初始化的.GetFormatCount函數用來檢測某種特定的格式是否可以被用來獲取或者設置數據.在獲取數據的時候,只有位圖和元文件格式是可以被處理的.GetDataSize函數依據請求的數據格式的不同返回合適的數據大小,如果必要的話,為了得到這個大小,你可以在這個時候創建位圖成員或者元文件成員.
```
virtual wxDataFormat GetPreferredFormat(Direction dir) const
{
return m_formatShape;
}
virtual size_t GetFormatCount(Direction dir) const
{
// 我們自定義的數據格式類型即可以支持 GetData()
// 也可以支持 SetData()
size_t nFormats = 1;
if ( dir == Get )
{
// 但是,位圖格式只支持輸出
nFormats += m_dobjBitmap.GetFormatCount(dir);
nFormats += m_dobjMetaFile.GetFormatCount(dir);
}
return nFormats;
}
virtual void GetAllFormats(wxDataFormat *formats, Direction dir) const
{
formats[0] = m_formatShape;
if ( dir == Get )
{
// 在獲取方向上我們增加位圖和元文件兩種格式的支持
//在Windows平臺上
m_dobjBitmap.GetAllFormats(&formats[1], dir);
// 不要認為m_dobjBitmap只有一種格式
m_dobjMetaFile.GetAllFormats(&formats[1 +
m_dobjBitmap.GetFormatCount(dir)], dir);
}
}
virtual size_t GetDataSize(const wxDataFormat& format) const
{
if ( format == m_formatShape )
{
return m_shape->GetDataSize();
}
else if ( m_dobjMetaFile.IsSupported(format) )
{
if ( !m_hasMetaFile )
CreateMetaFile();
return m_dobjMetaFile.GetDataSize(format);
}
else
{
wxASSERT_MSG( m_dobjBitmap.IsSupported(format),
wxT("unexpected format") );
if ( !m_hasBitmap )
CreateBitmap();
return m_dobjBitmap.GetDataSize();
}
}
```
GetdataHere函數按照請求的數據格式類型將數據拷貝到void*類型的緩沖區:
```
virtual bool GetDataHere(const wxDataFormat& format, void *pBuf) const
{
if ( format == m_formatShape )
{
// 使用ShapeDump結構將其轉換為void*流
m_shape->GetDataHere(pBuf);
return true;
}
else if ( m_dobjMetaFile.IsSupported(format) )
{
if ( !m_hasMetaFile )
CreateMetaFile();
return m_dobjMetaFile.GetDataHere(format, pBuf);
}
else
{
wxASSERT_MSG( m_dobjBitmap.IsSupported(format),
wxT("unexpected format") );
if ( !m_hasBitmap )
CreateBitmap();
return m_dobjBitmap.GetDataHere(pBuf);
}
}
```
SetData函數只需要處理本地格式,因此,它需要做的所有事情就是使用DndShape::New函數來制作一個參數圖形的拷貝:
```
virtual bool SetData(const wxDataFormat& format,
size_t len, const void *buf)
{
wxCHECK_MSG( format == m_formatShape, false,
wxT( "unsupported format") );
delete m_shape;
m_shape = DnDShape::New(buf);
// the shape has changed
m_hasBitmap = false;
m_hasMetaFile = false;
return true;
}
```
實現DndShape和void*類型的互相轉換的方法是非常直接的.它使用了一個ShapeDump的結構來保存圖形的詳細信息.下面是其實現方法:
```
// 靜態函數用來從一個void*緩沖區中創建一個圖形
DnDShape *DnDShape::New(const void *buf)
{
const ShapeDump& dump = *(const ShapeDump *)buf;
switch ( dump.k )
{
case Triangle:
return new DnDTriangularShape(
wxPoint(dump.x, dump.y),
wxSize(dump.w, dump.h),
wxColour(dump.r, dump.g, dump.b));
case Rectangle:
return new DnDRectangularShape(
wxPoint(dump.x, dump.y),
wxSize(dump.w, dump.h),
wxColour(dump.r, dump.g, dump.b));
case Ellipse:
return new DnDEllipticShape(
wxPoint(dump.x, dump.y),
wxSize(dump.w, dump.h),
wxColour(dump.r, dump.g, dump.b));
default:
wxFAIL_MSG(wxT("invalid shape!"));
return NULL;
}
}
// 返回內部數據大小
size_t DndShape::GetDataSize() const
{
return sizeof(ShapeDump);
}
// 將自己填入一個void*緩沖區
void DndShape::GetDataHere(void *buf) const
{
ShapeDump& dump = *(ShapeDump *)buf;
dump.x = m_pos.x;
dump.y = m_pos.y;
dump.w = m_size.x;
dump.h = m_size.y;
dump.r = m_col.Red();
dump.g = m_col.Green();
dump.b = m_col.Blue();
dump.k = GetKind();
}
```
最后,我們回到DnDShapeDataObject數據對象,下邊的這些函數用來在需要的時候將內部數據轉換為位圖或者元數據:
```
void DnDShapeDataObject::CreateMetaFile() const
{
wxPoint pos = m_shape->GetPosition();
wxSize size = m_shape->GetSize();
wxMetaFileDC dcMF(wxEmptyString, pos.x + size.x, pos.y + size.y);
m_shape->Draw(dcMF);
wxMetafile *mf = dcMF.Close();
DnDShapeDataObject *self = (DnDShapeDataObject *)this;
self->m_dobjMetaFile.SetMetafile(*mf);
self->m_hasMetaFile = true;
delete mf;
}
void DnDShapeDataObject::CreateBitmap() const
{
wxPoint pos = m_shape->GetPosition();
wxSize size = m_shape->GetSize();
int x = pos.x + size.x,
y = pos.y + size.y;
wxBitmap bitmap(x, y);
wxMemoryDC dc;
dc.SelectObject(bitmap);
dc.SetBrush(wxBrush(wxT("white"), wxSOLID));
dc.Clear();
m_shape->Draw(dc);
dc.SelectObject(wxNullBitmap);
DnDShapeDataObject *self = (DnDShapeDataObject *)this;
self->m_dobjBitmap.SetBitmap(bitmap);
self->m_hasBitmap = true;
}
```
我們自定義的數據對象的實現到此為止就全部完成了,部分細節(比如圖形怎樣把自己繪制到用戶界面上)沒有在此列出,你可以參考wxWidgets自帶的samples/dnd中的代碼.
wxWidgets中的拖放相關的一些幫助
下面我們來描述一些在實現拖放操作時可以給你幫助的控件.
wxTreeCtrl
你可以使用EVT_TREE_BEGIN_DRAG或EVT_TREE_BEGIN_RDRAG事件映射宏來增加對鼠標左鍵或右鍵開始的拖放操作的處理,這是這個控件內部的鼠標事件處理函數實現的.在你的事件處理函數中,你可調用wxtreeEvent::Allow來允許 wxtreeCtrl使用它自己的拖放實現來發送一個EVT_TREE_END_DRAG事件.如果你選擇了使用tree控件自己的拖放代碼,那么隨著拖放鼠標指針的移動,將會有一個小的拖動圖片被創建,并隨之移動,整個放置的操作則完全需要在應用程序的結束放置事件處理函數中實現.
下面的例子演示了怎樣使用樹狀控件提供的拖放事件,來實現當用戶把樹狀控件中的一個子項拖到另外一個子項上的時候,產生一個被拖動子項的拷貝.
```
BEGIN_EVENT_TABLE(MyTreeCtrl, wxTreeCtrl)
EVT_TREE_BEGIN_DRAG(TreeTest_Ctrl, MyTreeCtrl::OnBeginDrag)
EVT_TREE_END_DRAG(TreeTest_Ctrl, MyTreeCtrl::OnEndDrag)
END_EVENT_TABLE()
void MyTreeCtrl::OnBeginDrag(wxTreeEvent& event)
{
// 需要顯式的指明允許拖動
if ( event.GetItem() != GetRootItem() )
{
m_draggedItem = event.GetItem();
wxLogMessage(wxT("OnBeginDrag: started dragging %s"),
GetItemText(m_draggedItem).c_str());
event.Allow();
}
else
{
wxLogMessage(wxT("OnBeginDrag: this item can't be dragged."));
}
}
void MyTreeCtrl::OnEndDrag(wxTreeEvent& event)
{
wxTreeItemId itemSrc = m_draggedItem,
itemDst = event.GetItem();
m_draggedItem = (wxTreeItemId)0l;
// 在哪里拷貝這個子項呢?
if ( itemDst.IsOk() && !ItemHasChildren(itemDst) )
{
// 這種情況下拷貝到它的父項內
itemDst = GetItemParent(itemDst);
}
if ( !itemDst.IsOk() )
{
wxLogMessage(wxT("OnEndDrag: can't drop here."));
return;
}
wxString text = GetItemText(itemSrc);
wxLogMessage(wxT("OnEndDrag: '%s' copied to '%s'."),
text.c_str(), GetItemText(itemDst).c_str());
// 增加新的子項
int image = wxGetApp().ShowImages() ? TreeCtrlIcon_File : -1;
AppendItem(itemDst, text, image);
}
```
如果你想自己處理拖放操作,比如使用wxDropSource來實現,你可以在拖放開始事件處理函數中使用wxtreeEvent:: Allow函數來禁止默認的拖放動作,并且開始你自己的拖放動作.這種情況下拖放結束事件將不會被發送,因為你已經決定用自己的方式來處理拖放(如果使用 wxDropSource::DoDragDrop函數,你需要自己檢測何時拖放結束).
wxListCtrl
這個類沒有提供默認的拖動圖片,或者拖放結束事件,但是,它可以讓你知道什么時候開始一個拖放操作.使用 EVT_LIST_BEGIN_DRAG或EVT_LIST_BEGIN_RDRAG事件映射宏來實現你自己的拖放代碼.你也可以使用 EVT_LIST_COL_BEGIN_DRAG,EVT_LIST_COL_DRAGGING和EVT_LIST_COL_END_DRAG來檢測何時某一個單獨的列正在被拖動.
wxDragImage
在你實現自己的拖放操作的時候,可以很方便地使用wxDragImage類.它可以在頂層窗口上繪制一副圖片,還可以移動這個圖片,并且不損壞它后面的窗口.這通常是通過在移動之前保存一份背景窗口,并且在需要的時候,重繪背景窗口來實現的.
下圖演示了wxDragImage例子中的主窗口,你可以在wxWidgets的samples/dragimag中找到這個例子.當主窗口上的三個拼圖塊被拖動時,將會采用不同的拖動圖片,分別為圖片本身,一個圖標,或者一個動態產生的包含一串文本的圖片.如果你選擇使用整個屏幕這個選項,那么這個圖片可以被拖動到窗口以外的地方,在Windows平臺上,這個例子即可以使用標準的wxDragImage的實現(默認情形)來編譯,也可以使用本地原生控件來編譯,后者需要你在dragimag.cpp中將wxUSE_GENERIC_DRAGIMAGE置為1\.

當檢測到開始拖動操作的時候,創建一個wxDragImage對象,并且把它存放在任何你可以在整個拖動過程中訪問的地方,然后調用 BeginDrag來開始拖動,調用EndDrag來結束拖動.要移動這個圖片,第一次要使用Show函數,后面則需要使用Move函數.如果你需要在拖動過程當中刷新屏幕內容(比如在dragimag的例子中高量顯示某個項目),你需要先調用Hide函數,然后更新你的窗口,然后調用Move函數,然后調用Show函數.
你可以在一個窗口內拖動,也可以在整個屏幕或者屏幕的任何一部分內拖動,以節省資源.如果你希望用戶可以在兩個擁有不同的頂層父窗口的窗口之間拖動,你就必須使用全屏拖動的方式.全屏拖動的效果不一定完美,因為它在開始拖動的時候獲取了一副整個屏幕的快照,屏幕后續的改動則不會進行相應的更新.如果在你的拖動過程當中,別的應用程序對屏幕內容進行了改動,將會影響到拖動的效果.
在接下來的例子中,基于上面的那個例子,MyCanvas窗口顯示了很多DragShap類的圖片,它們中的每一個都和一副圖片綁定. 當針對某個DragShap的拖動操作開始時,一個使用其綁定的圖片的wxDragImage對象被創建,并且BeginDrag被調用.當檢測到鼠標移動的時候,調用wxDragImage::Move函數來移動來將這個對象進行相應的移動.最后,當鼠標左鍵被釋放的時候,用于指示拖動的圖片被釋放,被拖動的圖片則在其新的位置被重繪.
```
void MyCanvas::OnMouseEvent(wxMouseEvent& event)
{
if (event.LeftDown())
{
DragShape* shape = FindShape(event.GetPosition());
if (shape)
{
// 我們姑且認為拖動操作已經開始
// 不過最好等待鼠標移動一段時間再真正開始.
m_dragMode = TEST_DRAG_START;
m_dragStartPos = event.GetPosition();
m_draggedShape = shape;
}
}
else if (event.LeftUp() && m_dragMode != TEST_DRAG_NONE)
{
// 拖動操作結束
m_dragMode = TEST_DRAG_NONE;
if (!m_draggedShape || !m_dragImage)
return;
m_draggedShape->SetPosition(m_draggedShape->GetPosition()
+ event.GetPosition() - m_dragStartPos);
m_dragImage->Hide();
m_dragImage->EndDrag();
delete m_dragImage;
m_dragImage = NULL;
m_draggedShape->SetShow(true);
m_draggedShape->Draw(dc);
m_draggedShape = NULL;
}
else if (event.Dragging() && m_dragMode != TEST_DRAG_NONE)
{
if (m_dragMode == TEST_DRAG_START)
{
// 我們將在鼠標已經移動了一小段距離以后開始真正的拖動
int tolerance = 2;
int dx = abs(event.GetPosition().x - m_dragStartPos.x);
int dy = abs(event.GetPosition().y - m_dragStartPos.y);
if (dx <= tolerance && dy <= tolerance)
return;
// 開始拖動.
m_dragMode = TEST_DRAG_DRAGGING;
if (m_dragImage)
delete m_dragImage;
// 從畫布上清除拖動圖片
m_draggedShape->SetShow(false);
wxClientDC dc(this);
EraseShape(m_draggedShape, dc);
DrawShapes(dc);
m_dragImage = new wxDragImage(
m_draggedShape->GetBitmap());
// 被拖動圖片的左上角到目前位置的偏移量
wxPoint beginDragHotSpot = m_dragStartPos
m_draggedShape->GetPosition();
// 總認為坐標系為被捕獲窗口的客戶區坐標系
if (!m_dragImage->BeginDrag(beginDragHotSpot, this))
{
delete m_dragImage;
m_dragImage = NULL;
m_dragMode = TEST_DRAG_NONE;
} else
{
m_dragImage->Move(event.GetPosition());
m_dragImage->Show();
}
}
else if (m_dragMode == TEST_DRAG_DRAGGING)
{
// 移動這個圖片
m_dragImage->Move(event.GetPosition());
}
}
}
```
如果你希望自己繪制用于拖動的圖片而不是使用一個位圖,你可以實現一個wxGenericDragImage的派生類,重載其 wxDragImage::DoDrawImage函數和wxDragImage::GetImageRect函數.在非windows的平臺上, wxDragImage是wxGenericDragImage的一個別名而已,而windows平臺上實現的wxDragImage不支持 DoDrawImage函數,也限制只能繪制有時候顯得有點恐怖的半透明圖片,因此,你可以考慮在所有的平臺上都使用 wxGenericDragImage類.
當你開始拖動操作的時候,就在正準備調用wxDragImage::Show函數之前,通常你需要現在屏幕上擦除你要拖動的對象,這可以使得 wxDragImage保存的背景中沒有正在拖動的對象,因此整個的拖動過程看上去也更合理,不過這將導致屏幕在開始拖動的時候會有一點點的閃爍.要避免這種閃爍(僅適用于使用wxGenericDragImage的情況),你可以重載wxGenericDragImage的 UpdateBackingFromWindow函數,使用傳遞給你的設備上下文繪制一個不包含正在拖動對象的背景,然后你就不需要在調用 wxDragImage::Show函數之前擦除你要拖動的對象了,整個拖動過程的屏幕就將會是平滑而無閃爍的了.
- 第一章 介紹
- 1.1 為什么要使用wxWidgets?
- 1.2 wxWidgets的歷史
- 1.3 wxWidgets社區
- 1.4 wxWidgets和面向對象編程
- 1.5 wxWidgets的體系結構
- 1.6 許可協議
- 第一章小結
- 第二章 開始使用
- 2.1 一個小例子
- 2.2 應用程序類
- 2.3 Frame窗口類
- 2.4 事件處理函數
- 2.5 Frame窗口的構造函數
- 2.6 完整的例子
- 2.7 wxWidgets程序一般執行過程
- 2.8 編譯和運行程序
- 第二章小結
- 第三章 事件處理
- 3.1 事件驅動編程
- 3.2 事件表和事件處理過程
- 3.3 過濾某個事件
- 3.4 掛載事件表
- 3.5 動態事件處理方法
- 3.6 窗口標識符
- 3.7 自定義事件
- 第三章小結
- 第四章 窗口的基礎知識
- 4.1 窗口解析
- 4.2 窗口類概覽
- 4.3 基礎窗口類
- 4.4 頂層窗口
- 4.5 容器窗口
- 4.6 非靜態控件
- 4.7 靜態控件
- 4.8 菜單
- 4.9 控制條
- 第四章小結
- 第五章繪畫和打印
- 5.1 理解設備上下文
- 5.2 繪畫工具
- 5.3 設備上下文中的繪畫函數
- 5.4 使用打印框架
- 5.5 使用wxGLCanvas繪制三維圖形
- 第五章小節
- 第六章處理用戶輸入
- 6.1 鼠標輸入
- 6.2 處理鍵盤事件
- 6.3 處理游戲手柄事件
- 第六章小結
- 第七章使用布局控件進行窗口布局
- 7.1 窗口布局基礎
- 7.2 窗口布局控件
- 7.3 使用布局控件進行編程
- 7.4 更多關于布局的話題
- 第七章小結
- 第八章使用標準對話框
- 8.1信息對話框
- 8.2 文件和目錄對話框
- 8.3 選擇和選項對話框
- 8.4 輸入對話框
- 8.5 打印對話框
- 第八章小結
- 第九章創建定制的對話框
- 9.1 創建定制對話框的步驟
- 9.2 一個例子:PersonalRecordDialog
- 9.3 在小型設備上調整你的對話框
- 9.4 一些更深入的話題
- 9.5 使用wxWidgets資源文件
- 第九章小結
- 第十章使用圖像編程
- 10.1 wxWidgets中圖片相關的類
- 10.2 使用wxBitmap編程
- 10.3 使用wxIcon編程
- 10.4 使用wxCursor編程
- 10.5 使用wxImage編程
- 10.6 圖片列表和圖標集
- 10.7 自定義wxWidgets提供的小圖片
- 第十章小結
- 第十一章剪貼板和拖放操作
- 11.1 數據對象
- 11.2 使用剪貼板
- 11.3 實現拖放操作
- 第十一章小結
- 第十二章高級窗口控件
- 12.1 wxTreeCtrl
- 12.2 wxListCtrl
- 12.3 wxWizard
- 12.4 wxHtmlWindow
- 12.5 wxGrid
- 12.6 wxTaskBarIcon
- 12.7 編寫自定義的控件
- 第十二章小結
- 第十三章數據結構類
- 13.1 為什么沒有使用STL?
- 13.2 字符串類型
- 13.3 wxArray
- 13.4 wxList和wxNode
- 13.5 wxHashMap
- 13.6 存儲和使用日期和時間
- 13.7 其它常用的數據類型
- 第十三章小結
- 第十四章文件和流操作
- 14.1 文件類和函數
- 14.2 流操作相關類
- 第十四章小結
- 第十五章內存管理,調試和錯誤處理
- 15.1 內存管理基礎
- 15.2 檢測內存泄漏和其它錯誤
- 15.3 構建自防御的程序
- 15.4 錯誤報告
- 15.5 提供運行期類型信息
- 15.6 使用wxModule
- 15.7 加載動態鏈接庫
- 15.8 異常處理
- 15.9 調試提示
- 第十五章小結
- 第十六章編寫國際化程序
- 16.1 國際化介紹
- 16.2 從翻譯說起
- 16.3 字符編碼和Unicode
- 16.4 數字和日期
- 16.5 其它媒介
- 16.6 一個小例子
- 第十六章小結
- 第十七章編寫多線程程序
- 17.1 什么時候使用多線程,什么時候不要使用
- 17.2 使用wxThread
- 17.3 用于線程同步的對象
- 17.4 多線程的替代方案
- 第十七章小結
- 第十八章使用wxSocket編程
- 18.1 Socket類和功能概覽
- 18.2 Socket及其基本處理介紹
- 18.3 Socket標記
- 18.4 使用Socket流
- 18.5 替代wxSocket
- 第十八章小結
- 第十九章使用文檔/視圖框架
- 19.1 文檔/視圖基礎
- 19.2 文檔/視圖框架的其它能力
- 19.3 實現Undo/Redo的策略
- 第十九章小結
- 第二十章完善你的應用程序
- 20.1 單個實例和多個實例
- 20.2 更改事件處理機制
- 20.3 降低閃爍
- 20.4 實現聯機幫助
- 20.5 解析命令行參數
- 20.6 存儲應用程序資源
- 20.7 調用別的應用程序
- 20.8 管理應用程序設置
- 20.9 應用程序安裝
- 20.10 遵循用戶界面設計規范
- 20.11 全書小結