一、概述
代碼寫的久了,什么功能都想搞點兒模式。不知道是不是只有我一個人這么想的,做功能時不在是只為了完成功能,而是在完成功能的同時會去考慮可擴展性怎么樣?哪塊兒的代碼可以復用?又或者哪里需要留更多的接口?這兩個類之間是不是可以加一個協作類?總之各種想法都出來了。
假設說有這樣一種場景,我們通過一個入口界面,上邊有很多種按鈕,當我們點擊上邊的按鈕時,程序就會生成一個小窗口。這些小窗口他們的外在行為基本一致,只是窗口里邊展示的內容各不一樣。
總結一下應該是這樣的
- 窗體可以通過標題欄進行拖拽
- 鼠標移動到邊界可以進行放大縮小
- 鼠標選中時邊框高量顯示
- 標題欄具有名稱、關閉和各種各樣的菜單
- 不同功能窗體的菜單內容不一樣,展示風格也可能完全不同
- 標題欄和中心窗體也有交互
- 連續點擊一個按鈕時,生成同類型的窗體不能完全覆蓋
用過富途牛牛的同學應該對這個功能比較清楚一點,富途中的交易模塊也是如此,工具箱里支持各種小窗體生成,並且都有各自豐富的內容。
這么多繁雜的窗體,富途是怎么管理的呢?
本篇文章就來仔細挖掘下這個功能,我們也來實現一個小窗口管理功能。
二、效果展示
如效果圖所示,我們之前所說的幾個功能點都已經有了,並且在配色上比之前的demo程序也有改善,當然要投入使用還是需要設計師同學經過專業的指點。

三、功能類
為了實現這個窗口管理功能,思考的頭都快要炸掉了,不是因為功能有多復雜,而是作為一個開發人員,我想的太多了,總是會出現一種幻覺,產品經理可能會提出這樣那樣的新需求,測試也可能會給你報一個莫名其妙的問題。
最最關鍵的還是我們要對自己寫的代碼有一個基本要求。結構必須合理、減少冗余和耦合,讓其他人閱讀代碼時使用最小的成本,
為了做這個窗口管理功能,我也是想了好久,總是想着設計的更合理一些,別人在使用時就會更簡單一些,使用成本也會更小。
為此,我引入了好幾個關鍵類,都是輔助管理窗口的,下面先來一張類圖,這是我使用starUML畫的,不是特別規范,主要是為了說明這些類的功能,以及他們之間的一個關系,圖上都用注釋寫出了每個類的大概作用。

從圖上也可以看出來,類中就是一個簡單的名字,我沒有把類的接口都貼上去,一個是時間問題,另一個我也覺着不是特別有必要,因為我本身對這個畫圖工具使用就不是特別深入,所以這里就只提供了簡單的版本。大家湊合看吧,對理解設計思路還是有很大的幫助。
本篇文章主要講的是小窗口管理,但是這里主要是通過講述迷你報價
小窗口來說明怎么去管理炒雞多的小窗口
從上邊圖中可知,關鍵類有以下這些
- ISmall:業務窗口接口類,負責和SmallWidget進行通信
- MiniPriceSmall:迷你報價小窗口的中心窗口
- SmallWidget:小窗口模板類,這是所有小窗口的外殼類,包含了標題欄。迷你報價窗口就是構造了這么一個類,然后設置了一個中心窗口MiniPriceSmall。其他的業務窗口都是使用同樣的模式
- ViewModePanel:
迷你報價
工具菜單按下時,彈出的視圖列表,主要是為了修改顯示模式。仔細觀察效果展示中的gif圖,菜單'M'被點擊時,彈出了顯示模式
窗口,隨后在顯示模式窗口任意選項上單擊后,迷你報價
窗口上都出現了一行歡迎文本--感謝您的使用,您選擇了模式:X - SmallContext:
小窗口
管理上下文類,主要負責處理我們定制好的外殼類SmallWidget和業務窗口MiniPriceSmall(其他更多業務窗口)之間的消息通信 - SmallGroup:小窗口組管理者,主要負責磁力吸附這個功能,詳情參看高仿富途牛牛-組件化(二)-磁力吸附這篇文章
- SmallFactory:
小窗口
工廠,負責構造各式各樣的小窗口 - ToolBoxDialog:工具箱的實現沒有專門去寫文章講解,需要了解的可以參看高仿富途牛牛-組件化(三)-界面美化這篇文章,第三小節對工具箱做了簡單的講解
四、設計上的考慮
首先,我自己做的是一個組件化的功能,我需要考慮的東西也是更多的,不僅僅要把功能完成,更多的是要去考慮,別人用的時候代價怎么可以更小?可以復用的代碼,我盡量都提取出來,寫成共性的東西,其他開發在做相應業務模塊時,只需要考慮真的和他們有直接關系的地方。
這里我們就拿迷你報價
這個功能來說事,看看怎么去設計是合理的,或者說目前看起來是合理的,可能后期根據需求,我們的結構或許會進行一定的調整。
1、功能拆分
單獨拿出一個迷你報價
功能來說,可能100%的人都會說很簡單,這有什么難的,我剛參加工作那會兒百度百度也都能做出來。可是往往把一些簡單的東西,抽象抽來,讓程序變得更簡單這件事情本身就是一件比較難的事情。
迷你報價
這個窗口總的來說可以分為上下兩部分:上邊標題欄和下面內容展示區域
標題欄
標題欄是每一個小窗口都有的,因此這里我們肯定是要單獨提出來作為一個公有的東西,當其他開發做自選股、小時鍾、短線精靈這些小窗口時,標題欄的移動事件他們就根本不需要考慮了。
細心的同學可能會發現了,標題欄上還有一些不認識的按鈕,這些按鈕時干什么的呢?
雖然標題欄已經被公有化,但是標題欄上的按鈕可不是這么想的,他們的行為要根據底部內容窗口的類型決定,因此這里就有了一定的變化。
為了適應這個變化,我自己定義了一個接口類ISmall,主要是讓內容窗口進行繼承的,也就是說內容窗口需要重寫這個接口類的一些接口,來完成運行時的一些設定。
比如說,迷你報價
標題欄按鈕顯示有哪些?按鈕顯示時都有哪些功能?按鈕點擊時該有什么樣的相應?按鈕點擊后彈出的面板點擊被點擊了,內容窗口做何處理等等。
內容展示
內容展示窗口就比較簡單了,這里有2個選擇,繼承ISmall接口類或者不繼承
- 繼承-意味着內容窗口需要和外殼窗口進行消息通信
- 不繼承-我們就只是展示數據的,不需要進行消息通信
事實上大多數的內容窗口都是需要繼承ISmall這個接口類的,純展示而沒有交互的功能幾乎沒有。
2、關鍵類
ISmall
ISmall接口類是毫無疑問的關鍵類,它是一個純虛類,繼承它的類都必須實現它的所有接口,如下代碼所示
/**
* 簡介: 小窗口上的回調事件,需要通知中心窗體
*/
struct ISmallCall
{
virtual ViewMode ViewType(SmallToolType) = 0;
virtual void GetItems(QVector<DataItem> &) = 0;
virtual QSize GetSize() const = 0;
virtual void Notify(const QString &) = 0;
};
GetSize()、GetItems()這些接口都是獲取數據接口,外殼類SmallWidget,會根據當前中心窗口返回的定制數據去初始化自己。
比如說效果圖中展示的顯示模式
面板,這個面板中的數據內容,就是迷你報價類MiniPriceSmall重寫了getItems接口之后,給外殼類提供的數據,並且制定了事件觸發時使用的顯示模式為視圖。
迷你報價
類重寫的數據准備接口如下
void MiniPriceSmall::GetItems(QVector<DataItem> & items)
{
for (int i = 0; i < 9; ++i)
{
DataItem item;
item.name = 'A' + i;
item.type = item.name;
item.img = "./image/mainWindow/tquant-btn_normal.png";
items.append(item);
}
}
目前接口類中的接口還不是特別完善,根據后續業務功能的變化,接口肯定還會繼續增多。
MiniPriceSmall
這個類是迷你報價的數據展示類,他繼承自QWidget窗口類和ISmall接口類,界面搭建這里我就不說了,兩個原因:其一是這里的界面本身就是空的,其二也是比較關鍵的,中心窗口的內容是根據業務類型決定的,后期根據不同開發負責的功能自己去搭建。通常情況下這里都是別人已經封裝好了的控件,有必要的時候在使用一個簡單外殼類包一層即可。
除過界面之外,就是ISmall的接口實現類了,一部分接口是給外部SmallWidget提供數據的,而另一部分接口就是響應外部事件。
目前來說,響應外部接口事件就只有一個接口Notify,比較重要,消息響應時根據參數來區分消息類型
SmallWidget
小窗口類,為所有小窗口的外殼類,提供了定制化的標題欄和邊界縮放。功能實現不難,需要的可自行百度。
ViewModePanel
顯示模式
面板,主要是一些小窗口可以根據該面板的選項可以進行顯示模式的重組,並且可以切換窗體的大小等等。效果展示中我們只是修改了顯示的問題,不過實現起來思路是一樣的,最主要的是內容窗體已經接收到我們傳遞的消息。
SmallContext
這是一個比較關鍵的類,當時想了很久,到底要不要加。
這個類主要負責的功能就是同步小窗體外殼類SmallWidget和內容窗體類之間的消息傳遞。
- 當SmallWidget想要給內容窗體傳遞消息時,會通過ISmall接口類進行發送指令,因為內容窗體是繼承了這個ISmall接口類的
- 當內容窗體想給外殼發送消息時,是通過ISmall的接口調用來到達目的的,嚴格來說其實是外殼窗體主動跟內容窗體要的消息內容。
當SunPanel面板通過工廠類SmallFactory進行構造小窗口時,會把SmallWidget和構造好的內容窗體進行注冊,並開始接收SmallWidget發送過來的事件請求。
void SmallContext::RegisterSmallWidget(SmallWidget * widget, ISmallCall * call)
{
m_itemMap[widget] = call;
connect(widget, &SmallWidget::ClickedButton, this, &SmallContext::HandleEvent);
}
當工具欄點擊了'M'按鈕時,SMallWidget就會給SmallContext發送事件請求,並附帶相關參數,這里SmallWidget發送了顯示模式
選擇事件,下面是SmallContext的處理方式
void SmallContext::HandleEvent(int type, const QPoint & globalPos)
{
if (SmallWidget * widget = dynamic_cast<SmallWidget *>(sender()))
{
if (m_itemMap.contains(widget))
{
if (ISmallCall * call = m_itemMap[widget])
{
ViewMode view = call->ViewType(SmallToolType(type));
if (view == VM_VIEW)
{
ShowView(widget, call, globalPos);
}
}
}
}
}
SmallFactory
最后就是工廠類了,一看名字就知道,他是一個工廠,專門負責生產小窗口的,對工具箱來說,他只知道給SubPanel發送顯示一個小窗口這個指令,但是SubPanel自己其實也不會去真正的構造一個小窗口,原因其實很簡單,單個的構造一個小窗體其實沒問題,但是當小窗體種類多起來時,需要維護的消息就隨之增多。
重構之后的代碼看起來像這樣,當SubPanel類需要一個小窗口時,他只需要這么干就行
SmallWidget * smallWidget = m_pSmallFactory->CreateWidget(type, subContent);
subContent->AddSmallWidget(smallWidget);
smallWidget->move(m_pSmallFactory->GetPos(type, subContent));
smallWidget->show();
調用工程的CreateWidget方法,構造一個小窗口,然后加入到自己的布局中
這里還有一個比較關鍵的問題,就是窗口初始位置管理,我們在工廠里維護了一個位置偏移量,當連續請求同一類型的小窗口時,我們會給這個偏移量增加一個固定值,使得連續請求時,同一類型的小窗口獲取到的位置是有規律的變化
void SmallFactory::UpdateOffset(bool change, QWidget * parent)
{
if (change)
{
m_offset += QPoint(13, 13);
}
else
{
m_offset = QPoint(0, 0);
}
QRect r = parent->rect();
if ( m_offset.y() > r.height() / 4
|| m_offset.x() > r.width() / 4)
{
m_offset *= -1;
m_offset.setX(m_offset.x() - 15);
}
}
五、相關文章
![]() |
![]() |
很重要--轉載聲明
-
本站文章無特別說明,皆為原創,版權所有,轉載時請用鏈接的方式,給出原文出處。同時寫上原作者:朝十晚八 or Twowords
-
如要轉載,請原文轉載,如在轉載時修改本文,請事先告知,謝絕在轉載時通過修改本文達到有利於轉載者的目的。