轉載請說明原出處,謝謝~~:http://blog.csdn.net/zhuhongshu/article/details/38253297
第一部分
我在前一段時間研究了怎麼製作duilib的菜單,花了幾天時間以MenuDemo爲基礎做出個duilib的菜單以備自用,近些天在羣裏經常會碰到羣友問如何給MenuDemo增加消息響應,爲了避免重複的回答我特意寫這篇日誌,希望可以幫到需要之人,同時也介紹瞭如何美化菜單的效果、動態修改自身的狀態以及通過增加屬性來優化菜單的xml文件編寫過程,先截個圖展示一下效果。
本菜單的優點如下:
1、可以展現多級菜單
2、可內嵌自定義控件,並且控件可以向主窗體發送消息,如圖的紅色歎號就是個按鈕控件,可以製作酷狗音樂的托盤菜單的播放暫停按鈕和進度控制進度條。
3、菜單擁有陰影效果
4、菜單可以自定義前方顯示小圖標,並且可以控制圖標的大小和是否顯示
5、菜單可以根據是否擁有子菜單決定是否顯示小箭頭
6、菜單可以添加分割線
7、每個菜單項都可以實現check菜單的功能,而且check信息會被保存下來以備下次顯示
8、優化菜單的xml描述文件,編寫方便容易,如果要寫一個二級菜單,比如編寫圖片中的菜單測試4以及他的子菜單,只需如下代碼就可以了
9、可以通過鍵盤的按鈕控制菜單的選項
10、每個菜單項的高度寬度是任意調整的
<MenuElement text="菜單測試4" icon="right.png" iconsize="9,9" expland="true" >
<MenuElement text="菜單測試5" expland="true" icon="WebSit.png" iconsize="16,16">
<MenuElement text="菜單測試6" icon="Virus.png" iconsize="16,16"/>
<MenuElement text="菜單測試7" />
</MenuElement>
<MenuElement text="菜單測試8" expland="true">
<MenuElement text="菜單測試a" />
<MenuElement text="菜單測試b" />
</MenuElement>
</MenuElement>
第二部分
菜單介紹完了,先說說我的開發過程的準備階段。
大家知道duilib庫是個非常棒的開源庫,我個人很喜歡他,但是由於現在作者不維護了,導致現在庫中存在的bug無人料理,控件也相對不足,而菜單控件是軟件開發中常用的,duilib庫中雖然給出了MenuDemo,但是沒有增加消息響應,導致一些朋友不會使用他,我開始也是這樣,所以打算自己給duilib做一個菜單。
我接觸到的和duilib有關的菜單的例子有:原帶的MenuDemo、Alberl寫的簡易Menu、ListDemo中的MenuWnd、其他庫的已經編寫成熟的Menu.
我找了很多Menu的代碼,看了看他們的實現方法。我在這裏簡單的說一下他們的優缺點:
原帶的MenuDemo:
優點:1、實現了多級菜單
2、內嵌自定義控件
缺點:1、無消息響應功能
2、菜單的xml描述文件過於繁瑣,在其中實現小箭頭或者小圖標都要靠自己的佈局來實現,這點大家可以看他的xml文件。
Alberl寫的簡易Menu和ListDemo中的MenuWnd:
優點:1、使用簡單方便
2、外觀效果很好
缺點:1、只是一級菜單,無法滿足很多需要多級菜單的場合
2、沒有封裝爲菜單類,無法通過調節屬性來控制菜單的效果
3、菜單的邏輯和消息處理比較簡單,邏輯效果不夠完善
總結了他們的優缺點之後,我打算做出個更通用的菜單,更簡單,效果更好的菜單,他擁有多級菜單、內嵌自定義控件、可以實現消息響應、外
觀效果要好、可以通過簡單的xml文件描述就把菜單編寫出來、可以顯示小圖標和小箭頭、菜單內實現check的功能,並且可以把是否被check的信息
保存下來。
第三部分
現在說一下我的編寫過程,開始我想自己寫一個菜單,但是我寫到一半的時候就被難倒了,由於我個人水平的限制。多級菜單的實現我一直無法
完美搞定,看似簡單的東西,其實裏面包含很多邏輯判斷:多級菜單的顯示位置,顯示大小,如何通知上級菜單自己被銷燬,怎麼發送通知,何時銷燬
自己,應該顯示那個下級菜單,怎麼保存子菜單的信息。實在搞不下去我只好去求救於現成的MenuDemo,他的最大優點就是已經寫好的多級菜單的
邏輯,而且並不需要再修改這部分邏輯,而這正是我認爲最難實現的部分,所以我就直接在MenuDemo的基礎上進行下一步開發,作者用觀察者模式
很好的實現了這個功能,前人栽樹後人乘涼,在此謝過編寫MenuDemo的作者。
接下在我先吧多數人最關心的怎麼發送並響應菜單的消息的部分說明一下。
要實現消息響應有三種方法:1、通過給菜單增加Notify接口實現 2、通過給菜單項增加委託實現 3、通過系統的消息機制實現
實際上第一種和第二種的方法是類似的,而第三種方法最麻煩但是效果和性能是做好的。
我先介紹第一種和第二種方法
通過讀MenuDemo的代碼可以知道,菜單實際上是一個彈出窗體,既然我們的主窗體可以響應控件的消息,那麼這個彈出窗體同樣可以實現,我
們要做到就是在菜單類CMenuWnd中繼承消息通知接口INotifyUI,這樣PaintManager類就會在合適的時候把消息發送到繼承了這個接口的窗體上,我
們要在CMenuWnd類中實現Notify函數,這個大家應該很熟悉,然後我們在CMenuWnd類的HandleMessage函數的WM_CREATE消息處理中增加這個
代碼,m_pm.AttachDialog(pRoot);(如果不知道在哪裏添加,可以搜索語句m_pm.AttachDialog(pRoot);,在這句代碼下添加,記住有兩處需要添
加),增加了這個代碼後PaintManager類在發現消息後就會把這個消息分發到擁有Notify函數的窗體中,這樣我們就能在CMenuWnd類的Notify函數中
接收到菜單被單擊的消息了,這個處理方法大家應該很熟悉的。(PS:ListDemo就是用這個方法來處理消息,如果還不會的朋友可以看一下他的
代碼)
第二種方法是通過委託,這是模仿c#的功能,實際上也就是通過巧妙的回調函數實現的技術。當我們創建了菜單後,在CMenuUI類的Add和AddAt
函數中增加類似這樣的代碼:
pMenuItem->OnNotify += MakeDelegate(this, &CMenuUI::OnMenuElemrntClick);
然後我們在CMenuUI類中增加這種形式的函數bool Fn(void*);比如例子中的函數爲bool OnMenuElemrntClick(void* prarm);函數體如下
bool OnMenuElemrntClick(void* param)
{
TNotifyUI* pMsg = (TNotifyUI*)param;
if( pMsg->sType == _T("itemclick"))
{
//處理消息
}
return true;
}
這樣就能接收到消息,接收到消息後就是如何把消息發送到主窗體讓他來處理消息,爲此我們要在主窗體創建菜單時把任意一個控件的指針傳到
菜單裏面,然後菜單裏保存這個指針,比如爲m_pOwner,然後在Notify函數或者OnMenuElemrntClick函數裏,首先判斷出是單擊了那個菜單項,然後
使用如下代碼把消息發送到主窗體:
if( m_pOwner )
m_pOwner->GetManager()->SendNotify(m_pOwner, _T("菜單消息"), 0, 0, true);
回到主窗體,在主窗體的Notify函數裏就可以接收到這個消息了,接收的demo如下
if( msg.sType == _T("菜單消息") ) {
//收到了菜單的消息
}
至此,第一種和第二種方法就介紹完了,我最開始就是使用這個方法發送消息的,但是注意這個方法有缺陷:不能在主窗體的接收函數中做出
會導致菜單銷燬的事情,比如最常見的彈出Messagebox或者模式窗體,否則程序會崩潰。原因很簡單,因爲首先是菜單發送消息給主窗體,然
後主窗體收到消息,但是注意這是菜單還有剩餘代碼沒有執行完,如果這時彈出模式窗體,菜單就會因爲失去焦點而銷燬,等到彈出的模式窗體銷燬
後,程序會執行菜單裏沒有執行完的代碼,但是此時菜單已經被銷燬,所以程序就崩潰了,切記!
第三種方法
這是我現在使用的也是推薦給大家的方法,就是使用系統的消息機制來將菜單項的單擊發送給主窗體,這個方法快速有效,而且沒有第一種和第
二種方法的缺陷。
首先我們在菜單類的頭文件中自定義一個小,比如我的是
#define WM_MENUCLICK WM_USER +121 //用來接收按鈕單擊的消息
然後我們在CMenuElementUI類的每一個響應單擊的位置加上這句代碼
PostMessage(s_context_menu_observer.m_hMainWnd, WM_MENUCLICK, (WPARAM)(new CDuiString(GetName().GetData())), (LPARAM)0);
這樣就把消息發送到主窗體,爲此我們需要在菜單建立時把主窗體的句柄傳進來並且保存,我把這個句柄保存到了s_context_menu_observer,
因爲他是原demo自帶的一個全局變量,很好的滿足 了我們的需求。這裏注意我是把菜單的Name傳送了過去而不是傳送ID,這符合duilib的使用習慣。
注意傳送字符串我使用了new,在處理了字符串後一定要記得delete他,否則會內存泄漏!
接着我們在主窗體裏接收這個消息,只要我們的窗體是繼承自WindowImplBase類,我們就可以在窗體類中重寫HandleCustomMessage函數,這
個函數本意就是讓我們來操作自定義消息的,如果不是繼承自WindowImplBase類,那就直接在HanddlMessage函數裏響應了。收到了這個消息,想怎
麼處理都隨你了。
至此,我就介紹完了最關鍵的怎麼響應消息的部分,我做得那個菜單同時使用了第一種和第三種方法,用第一種方法來發送自動一控件的消息,
如播放被單擊;用第三種方法來傳遞菜單項的單擊消息,這樣菜單的消息傳送就完全了。
第四部分
優化代碼和顯示效果
可以看到MenuDemo的xml文件的編寫實在是複雜難懂,他只是提供了個容器,裏面顯示的內容完全要靠自己的佈局來完成,這樣過於繁瑣,所以
我通過增加屬性的方法簡化了這個過程,可以在文章的篇頭看到我的那個xml代碼比原demo的xml少多了。
爲了減少代碼量,我給菜單項增加了icon、 iconsize、ischeck、expland四個屬性,分別控制顯示的小圖標,圖標的大小、是否被選中,是否可以
展開。然後在類中增加了相應的函數,用來繪製小圖標,擴展圖標,繪製的時候要根據自己所在的菜單項的高度寬度和自己的大小來調整合適的位置,
還有改不改繪製出來。這樣子就簡化了xml的編寫,不過說起來簡單,我做的時候在處理小圖標和擴展圖標的顯示的先後順序上也花了點時間,估計是
自己的邏輯還不夠清晰。
另外我還增加了一個屬性"type",當屬性值爲"line"時, 就繪製一條橫線,繪製的橫線的顏色、寬度等信息是從菜單的Default標籤中獲取的。
接着爲了給菜單增加陰影效果,我使用了CShadowWnd類,這個類的用法和demo我在多個duilib羣裏都上傳過,就不再贅述了。
最後再說一下動態修改自己的狀態,爲了給菜單增加check的功能,我們要保存下來每個菜單項的check狀態,而菜單每次都是重新創建的,所以
這個狀態信息不能在菜單本身保存,我在主窗體中聲明一個map<CDuiString,bool>模版的變量用來保存菜單項的狀態,第一個字段保存菜單的名稱,
用來區別不同的菜單項,第二個字段來區分菜單是否被選中,創建菜單時這個變量傳進去,然後讓菜單自己來維護這個變量,當菜單項被單擊時,他會
從變量中找到自己並且修改爲應有的值,菜單在創建時會讀取這個變量找到自己的信息來決定是否繪製小圖標。
結束語
至此,我的這篇日誌就結束了,我大致把自己實現這個菜單的思路講了一下,但是由於我的個人水平的原因,可能代碼邏輯並不好,我的說明也
不夠清晰。如果你有什麼建議,非常歡迎給我提出來,可以加我QQ,也可以留言。現在的菜單控件我已經重構,並且上傳了源碼。
附上窗體陰影的代碼和Demo:點擊打開鏈接
此菜單控件最新的代碼和使用demo已經開源,詳見開源博文 :點擊打開鏈接
2014.7.29 10:41 Redrain