原文連接:Qt之股票組件-股票檢索--支持搜索結果預覽、鼠標、鍵盤操做windows
以前作過一款炒股軟件,我的覺着是我職業生涯裏作過的效果最好的一款產品,並且速度也不慢,效果能夠參考財聯社-產品展現這篇文章,固然這篇文章只能顯示有限的內容,其中整個代碼的結構、一些好的方法和設計模式是沒有機會展現的。設計模式
最近聽到一個很差的消息,咱們的產品夭折了。剛聽到這個消息時心理還挺不是滋味的,畢竟這個產品我是從頭參與到尾,後來由於種種緣由離開了,產品功能也就此終結,但回想起那段開發的日子,真的是收穫滿滿。更確切的說,這個產品應該是換了一種語言從新開始作。網絡
不爽歸不爽,可整個產品的代碼仍是不錯的,所以 後續有時間我會慢慢的把一些好的代碼抽離出來,編譯成一個個能夠單獨運行的demo,方便有須要的朋友使用。app
若是有須要的朋友能夠加我好友,有償提供源碼、或者也能夠進一步提供功能定製less
封裝的控件,或者demo都是沒有樣式的,因此看着會比較醜一些,不過加樣式也是分分鐘。。。這裏咱能夠先看功能,須要便可定製ide
本篇文章咱們首先介紹的就是股票,該控件支持經常使用的股票檢索功能,支持模糊匹配,鍵盤上下鍵切換當前檢索項等函數
右鍵菜單包括複製、粘貼、剪貼、全選等工具
本篇文章中不包括的功能也能夠提供定製,需求合理便可。組件化
下面來具體說一說這個功能的實現思路,會公開大多數核心代碼,有須要的同窗能夠根據思路自行完善整個代碼。
以下效果圖所示,是自選股使用上的一個展現效果,具備以下功能
若是覺着demo比較醜的話,能夠看財聯社-產品展現這篇文章中的效果圖
首先出場的是搜索編輯框,如gif圖中展現所示,搜索框支持預覽數據,當咱們輸入了字符串後,就會出現過濾後的預覽數據。這裏因爲咱們的股票數據是我本身模擬的,所以只顯示了5條數據。
實現搜索編輯框,有2個小的模塊須要講解,一個是編輯框自己,它用於輸入文本的能力,而且支持複製、粘貼等交互操做;另外一個就是預覽框了,他會動態的展現當前搜索的內容。
Qt已經幫咱們實現了一種編輯框,可是他自帶了不少菜單項,若是產品這個時候說,菜單項我須要本身定製,多餘的項不要。那麼咱們是否是得重寫這個控件呢?答案是確定的
下面咱們就來說解這個控件的重寫步驟
重寫一個Qt控件仍是很簡單的,使用Qt超過半年的同窗都會重寫大量各類各樣的控件,而咱們的編輯框重寫就會像下面這樣,是一個簡單的頭文件展現
///***********************************/// /// 描述:自定義編輯框,重寫鼠標右鍵事件 ///***********************************/// class SearchEdit : public QLineEdit { public: SearchEdit(QWidget * parent = nullptr); ~SearchEdit(){} protected: virtual void contextMenuEvent(QContextMenuEvent * event) override; private: void InitMenu(); private: QMenu * m_PopMenu = nullptr; };
這裏咱們主要是針對右鍵菜單進行了重寫,Qt窗體實現右鍵菜單的方式多種多樣,具體能夠參考我很早之前寫的Qt之自定義QLineEdit右鍵菜單這篇文章,今天咱們也使用其中的一種方式來實現右鍵菜單,那就是實現默認的contextMenuEvent函數,這個函數之因此會響應,也是有必定條件的,Qt之自定義QLineEdit右鍵菜單這篇文章中講解的也很清楚,那就是contextMenuPolicy的值必須爲默認的Qt::DefaultContextMenu屬性。
至於菜單重寫實現函數,這裏就不展現了,就是比較常規的使用QMenu增長QAction的操做
你們仔細想想,預覽框是何時出現的?他顯示的數據有什麼樣的特徵?接下來咱們來一一作以分析
首先是出現時機
預覽框主要是展現咱們模糊搜索後的股票數據,那麼結論就很明顯了。預覽的出現時機就是搜索內容發現變化的時候,而且當編輯框失去焦點時,咱們應該主動關閉預覽框
編輯框內容發現變化時,顯示預覽框
connect(d_ptr->m_pSearchLineEdit, &QLineEdit::textChanged, this, &SelfStocksWidget::TextChanged);
處理預覽框數據,主要是使用了FilterModel來進行過濾全部股票後選項,注意咱們過濾的條件就是搜索框中輸入的內容
void SelfStocksWidget::TextChanged(const QString & text) { if (d_ptr->m_pFilterModel) { d_ptr->m_pFilterModel->SetFilterContext(text); } if (d_ptr->m_pStockPreviewWidget) { if (text.isEmpty()) { d_ptr->m_pStockPreviewWidget->hide(); d_ptr->m_pPreviewError->hide(); d_ptr->m_pCloseButton->setIcon(QIcon()); } else { d_ptr->m_pCloseButton->setIcon(QIcon(":/optional/Resources/optional/sotck_search_close_normal.png")); d_ptr->m_pStockPreviewWidget->move(d_ptr->m_pTitleWidget->mapToGlobal(QPoint(0, d_ptr->m_pTitleWidget->height()))); int rowHeight = d_ptr->m_pStockPreview->rowHeight(0); int rowCount = d_ptr->m_pFilterModel->rowCount(); ... } } }
當編輯框失去焦點時,關閉預覽框
這裏咱們取了一個巧,接收了該App的原生Win32消息,當咱們發現一些影響窗口焦點的事件被觸發時,咱們去判斷是否須要關閉預覽框。
具體能夠參考我很早以前寫的qt捕獲全局windows消息這篇文章
bool SelfStocksWidget::nativeEventFilter(const QByteArray & eventType, void * message, long * result) { if (eventType == "windows_generic_MSG" || eventType == "windows_dispatcher_MSG") { MSG * pMsg = reinterpret_cast<MSG *>(message); if (pMsg->message == WM_MOVE) { NativeParentWindowMove(); } else if (pMsg->message == WM_ACTIVATEAPP) { if (bool(pMsg->wParam) == false) { if (!d_ptr->m_pStockPreviewWidget->rect().contains(d_ptr->m_pStockPreview->mapFromGlobal(QPoint(pMsg->pt.x, pMsg->pt.y)))) { d_ptr->m_pStockPreviewWidget->hide(); } if (!d_ptr->m_pPreviewError->rect().contains(d_ptr->m_pPreviewError->mapFromGlobal(QPoint(pMsg->pt.x, pMsg->pt.y)))) { d_ptr->m_pPreviewError->hide(); } } } else if (pMsg->message == WM_NCMBUTTONDOWN || pMsg->message == WM_LBUTTONDOWN || pMsg->message == WM_RBUTTONDOWN || pMsg->message == WM_NCLBUTTONDOWN || pMsg->message == WM_NCRBUTTONDOWN || pMsg->message == WM_MBUTTONDOWN) { 同上... }
下面就是一個比較負責預覽數據環節了,幾千只股票,要準、要快,咱們應該怎麼技術選型呢?
預覽框到底怎麼顯示數據的?他顯示的都是哪些數據?
Qt提供了QListView、QTableView和QTreeView這3種視圖模式,而後搭配Mode數據源,能夠完成高效的大量數據展現,得知這個內容後是否是還有些小興奮呢!
乍一看,QListView和QTableView均可以做爲咱們的預覽框窗口,畢竟每個Item項都是能夠去從新定製的,看起來QListView仍是更簡單一些,並且速度也會更快一些,可是仔細想一想,好像不是這麼回事,咱們既然要支持股票代碼和名稱都進行搜索,那麼天然不是一列數據就能夠進行過濾的,方便起見咱們仍是使用QTableView做爲咱們的視圖窗口
既然視圖窗口選定了,接下來就是一堆的事件定製了
a、重寫QTableView
重寫QTableView時,咱們得考慮一個很重要的事情,那就是鼠標hover事件了,鼠標移動時咱們須要把當前行設置爲鼠標hover狀態,爲了實現這個效果,我可謂是費勁腦汁,想出了一個辦法,寫了一個IView接口類,讓QTableView去繼承,當鼠標hover時,去調用這個接口類告知QTableView當前hover項。
class IView { public: virtual void SetMouseHover(int, bool forceChanged = false) = 0; };
上邊的代碼是否是看着很簡單呢,就一個接口,就是當鼠標hover時告知表格當前hover項,那麼什麼實際通知合適呢?我這裏是重寫了QStyledItemDelegate繪圖代理類,在paint函數中通知表格的,其餘同窗有好的辦法也能夠留言。
預覽框的頭文件大體是下面這樣的,這裏我只把公有的接口放出來了,其餘的一些私有接口和成員變量沒有公開(放出來估計你們也不看)
///***********************************/// /// 描述:搜索預覽框 ///***********************************/// class StockTableView : public QTableView, public IView { Q_OBJECT signals : void RowClicked(const QString & code); void RowDbClicked(const QString & code); public: StockTableView(QStandardItemModel * model, QWidget * parent = 0); public: void SetMouseHover(int, bool forceChanged = false); void SetMouseChecked(int); void SetDbClickedEnable(bool enable); void SetHoverColor(const QColor & color); void SetCheckedColor(const QColor & color); void CheckedMoveUp(); void CheckedMoveDown(); void EnterPressed(); protected: ... private: ... };
代碼中的接口都比較好理解,看名字應該都知道是幹嗎的,這裏就不作過多解釋。
b、表格初始化
表格的數據內容在m_pListModel中存放,可是表格直接接收數據的是m_pFilterModel對象。
m_pFilterModel對象能夠理解爲是一個映像數據源,他沒有真正的去存儲數據,他的數據都是來自m_pListModel類。
//初始化搜索個股列表 d_ptr->m_pStockPreview = new StockTableView(d_ptr->m_pListModel); d_ptr->m_pFilterModel = new StockSortFilterProxyModel; d_ptr->m_pPreviewError->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); d_ptr->m_pPreviewError->setText(QStringLiteral("未搜索到相關股票")); d_ptr->m_pStockPreview->horizontalHeader()->setVisible(false); d_ptr->m_pStockPreview->verticalHeader()->setVisible(false); d_ptr->m_pStockPreview->setShowGrid(false); d_ptr->m_pStockPreview->horizontalHeader()->setStretchLastSection(true); d_ptr->m_pStockPreview->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); d_ptr->m_pStockPreview->setMouseTracking(true); previewLayout->addWidget(d_ptr->m_pStockPreview); d_ptr->m_pStockPreviewWidget->setLayout(previewLayout); StockItemDelegate * itemDelegate = new StockItemDelegate(d_ptr->m_pStockPreview); d_ptr->m_pStockPreview->setItemDelegate(itemDelegate); itemDelegate->setView(d_ptr->m_pStockPreview); d_ptr->m_pPreviewError->setWindowFlags(Qt::WindowStaysOnTopHint | Qt::Tool | Qt::FramelessWindowHint); d_ptr->m_pStockPreviewWidget->setWindowFlags(Qt::WindowStaysOnTopHint | Qt::Tool | Qt::FramelessWindowHint); d_ptr->m_pFilterModel->setSourceModel(d_ptr->m_pListModel); d_ptr->m_pStockPreview->setModel(d_ptr->m_pFilterModel); d_ptr->m_pStockPreview->setColumnHidden(2, true); d_ptr->m_pStockPreview->setSortingEnabled(true); d_ptr->m_pPreviewError->setFixedSize(DropWidgetMaxWidth, 26); d_ptr->m_pStockPreviewWidget->setFixedWidth(DropWidgetMaxWidth);
c、表格填充數據
正常來講數據應該是網絡上拉取的,可是這裏做爲測試,我直接添加了5行模擬數據
void SelfStocksWidget::InitiAStock() { std::vector<BaseStockInfoItem> sotckLists; BaseStockInfoItem item; for (int i = 1; i <= 5; ++i) { item.wstrSymbol = QString("0h000%1").arg(i).toStdWString(); item.wstrName = QString("%1%1%1").arg(i).toStdWString(); item.wstrSymbol = QString("pingyin%1").arg(i).toStdWString(); sotckLists.push_back(item); } for each (BaseStockInfoItem stock in sotckLists) { QList<QStandardItem *> rows; QStandardItem * symbol = new QStandardItem(QString::fromStdWString(stock.wstrSymbol).toUpper()); symbol->setData(QColor(28, 30, 34), Qt::BackgroundRole); symbol->setData(QColor(204, 204, 204), Qt::ForegroundRole); symbol->setSelectable(false); rows << symbol; QStandardItem * name = new QStandardItem(QString::fromStdWString(stock.wstrName)); name->setData(QColor(28, 30, 34), Qt::BackgroundRole); name->setData(QColor(204, 204, 204), Qt::ForegroundRole); name->setSelectable(false); rows << name; QStandardItem * pinyin = new QStandardItem(QString::fromStdWString(stock.wstrShortPinYin)); pinyin->setData(QColor(28, 30, 34), Qt::BackgroundRole); pinyin->setData(QColor(204, 204, 204), Qt::ForegroundRole); pinyin->setSelectable(false); rows << pinyin; //QStandardItem * type = new QStandardItem(QString::number(stock.m_stockType)); //type->setData(QColor(28, 30, 34), Qt::BackgroundRole); //type->setData(QColor(204, 204, 204), Qt::ForegroundRole); //type->setSelectable(false); //rows << type; d_ptr->m_pListModel->appendRow(rows); } }
最終的數據被填充到了m_pListModel數據源中。
d、鍵盤操做
文章開始的地方也說過了,咱們的搜索預覽框是支持鍵盤上下鍵來切換當前股票的,這個又是怎麼完成的呢!
預覽框顯示時,編輯框一直處於鼠標輸入狀態,而且具備鍵盤有限處理權限。
所以裏咱們是取了個巧,把編輯框的事件掛載在了他的父窗體上,當鍵盤按下時,父窗口拿到鍵盤按下事件,首先轉發給了預覽框,讓預覽框去換一個最新的當前股票,並選中。
代碼以下所示,是否是也很簡單。
bool SelfStocksWidget::eventFilter(QObject * watched, QEvent * event) { if (d_ptr->m_pSearchLineEdit == watched) { if (event->type() == QEvent::KeyPress) { if (QKeyEvent * keyEvent = static_cast<QKeyEvent *>(event)) { switch (keyEvent->key()) { case Qt::Key_Up: d_ptr->m_pStockPreview->CheckedMoveUp(); break; case Qt::Key_Down: d_ptr->m_pStockPreview->CheckedMoveDown(); break; case Qt::Key_Enter: case Qt::Key_Return: d_ptr->m_pStockPreview->EnterPressed(); break; default: break; } } } } return __super::eventFilter(watched, event); }
e、過濾
前邊也講述過了,咱們表格數據都是來自m_pFilterModel對象的,數據源中的數據m_pListModel基本沒有發生變化過,及時咱們現實的內容變化了,那也僅僅是m_pFilterModel對象過濾到的內容發生了變化。
過濾接口Qt已經幫咱們寫好了,咱們只須要實現其中的過濾方式便可。
bool StockSortFilterProxyModel::filterAcceptsRow(int source_row , const QModelIndex & source_parent) const { QRegExp regExp = filterRegExp(); if (regExp.isEmpty()) { return true; } bool result = false; for (int i = 0; i < sortColumn; ++i) { QModelIndex index = sourceModel()->index(source_row, i, source_parent); QString context = sourceModel()->data(index).toString(); QString regExpStr = regExp.pattern(); result = regExp.exactMatch(context); if (result) { break; } } return result; }
以上就是搜索股票編輯框的大體內容了,至於一些細微的設置,你們自行去完善便可。
好比說預覽框的窗口屬性應該是這樣的:
setWindowFlags(Qt::WindowStaysOnTopHint | Qt::Tool | Qt::FramelessWindowHint);
未輸入任務內容時,編輯框的holderText應該是這樣的:
setPlaceholderText(QStringLiteral("搜索股票代碼/名稱"));
因爲篇幅緣由,本篇文章就只先說搜索編輯框吧,原本想把自選股列表頁一塊兒加上,不過覺着內容太多,也不利於你們吸取,下一篇文章補上吧。。。
寫的手都酸了,其餘內容自行腦補吧。。。
高仿富途牛牛-組件化(六)-炒雞牛逼的佈局記憶功能(序列化和反序列化)
很重要--轉載聲明