原文连接: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("搜索股票代码/名称"));
因为篇幅缘由,本篇文章就只先说搜索编辑框吧,原本想把自选股列表页一块儿加上,不过觉着内容太多,也不利于你们吸取,下一篇文章补上吧。。。
写的手都酸了,其余内容自行脑补吧。。。
高仿富途牛牛-组件化(六)-炒鸡牛逼的布局记忆功能(序列化和反序列化)
![]() |
![]() |
很重要--转载声明