Qt之股票组件-股票检索--支持搜索结果预览、鼠标、键盘操作
原文链接:Qt之股票组件-股票检索--支持搜索结果预览、鼠标、键盘操作
一、感慨一下
之前做过一款炒股软件,个人觉着是我职业生涯里做过的效果最好的一款产品,而且速度也不慢,效果可以参考财联社-产品展示这篇文章,当然这篇文章只能显示有限的内容,其中整个代码的结构、一些好的方法和设计模式是没有机会展示的。
最近听到一个不好的消息,我们的产品夭折了。刚听到这个消息时心理还挺不是滋味的,毕竟这个产品我是从头参与到尾,后来因为种种原因离开了,产品功能也就此终结,但回想起那段开发的日子,真的是收获满满。更确切的说,这个产品应该是换了一种语言重新开始做。
不爽归不爽,可整个产品的代码还是不错的,因此 后续有时间我会慢慢的把一些好的代码抽离出来,编译成一个个可以单独运行的demo,方便有需要的朋友使用。
如果有需要的朋友可以加我好友,有偿提供源码、或者也可以进一步提供功能定制
封装的控件,或者demo都是没有样式的,所以看着会比较丑一些,不过加样式也是分分钟。。。这里咱可以先看功能,需要即可定制
本篇文章我们首先介绍的就是股票,该控件支持常用的股票检索功能,支持模糊匹配,键盘上下键切换当前检索项等
右键菜单包括复制、粘贴、剪贴、全选等
本篇文章中不包括的功能也可以提供定制,需求合理即可。
下面来具体说一说这个功能的实现思路,会公开大多数核心代码,有需要的同学可以根据思路自行完善整个代码。
二、效果展示
如下效果图所示,是自选股使用上的一个展示效果,具有如下功能
- 搜索编辑框,支持股票代码和股票名称搜索
- 搜索预览框支持鼠标hover,并且可以使用键盘上下键进行当前项切换,单机时支持切换自选股
- 自选股列表,支持拖拽,拖拽时会有拖拽项映像,并示意将要拖拽到哪个位置
- 支持右键菜单,可以对某一项进行移动,删除等操作
如果觉着demo比较丑的话,可以看财联社-产品展示这篇文章中的效果图
三、搜索编辑框
首先出场的是搜索编辑框,如gif图中展示所示,搜索框支持预览数据,当我们输入了字符串后,就会出现过滤后的预览数据。这里由于我们的股票数据是我自己模拟的,因此只显示了5条数据。
实现搜索编辑框,有2个小的模块需要讲解,一个是编辑框本身,它用于输入文本的能力,并且支持复制、粘贴等交互操作;另一个就是预览框了,他会动态的展示当前搜索的内容。
1、编辑框
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的操作
2、预览框
大家仔细想一想,预览框是什么时候出现的?他显示的数据有什么样的特征?接下来我们来一一做以分析
首先是出现时机
预览框主要是展示我们模糊搜索后的股票数据,那么结论就很明显了。预览的出现时机就是搜索内容发现变化的时候,并且当编辑框失去焦点时,我们应该主动关闭预览框
编辑框内容发现变化时,显示预览框
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("搜索股票代码/名称"));
由于篇幅原因,本篇文章就只先说搜索编辑框吧,本来想把自选股列表页一起加上,不过觉着内容太多,也不利于大家吸收,下一篇文章补上吧。。。
写的手都酸了,其他内容自行脑补吧。。。
四、相关文章
高仿富途牛牛-组件化(六)-炒鸡牛逼的布局记忆功能(序列化和反序列化)
很重要--转载声明
-
本站文章无特别说明,皆为原创,版权所有,转载时请用链接的方式,给出原文出处。同时写上原作者:朝十晚八 or Twowords
-
如要转载,请原文转载,如在转载时修改本文,请事先告知,谢绝在转载时通过修改本文达到有利于转载者的目的。