Qt之股票组件-股票检索--支持搜索结果预览、鼠标、键盘操作

原文链接:Qt之股票组件-股票检索--支持搜索结果预览、鼠标、键盘操作

一、感慨一下

之前做过一款炒股软件,个人觉着是我职业生涯里做过的效果最好的一款产品,而且速度也不慢,效果可以参考财联社-产品展示这篇文章,当然这篇文章只能显示有限的内容,其中整个代码的结构、一些好的方法和设计模式是没有机会展示的。

最近听到一个不好的消息,我们的产品夭折了。刚听到这个消息时心理还挺不是滋味的,毕竟这个产品我是从头参与到尾,后来因为种种原因离开了,产品功能也就此终结,但回想起那段开发的日子,真的是收获满满。更确切的说,这个产品应该是换了一种语言重新开始做。

不爽归不爽,可整个产品的代码还是不错的,因此 后续有时间我会慢慢的把一些好的代码抽离出来,编译成一个个可以单独运行的demo,方便有需要的朋友使用。

如果有需要的朋友可以加我好友,有偿提供源码、或者也可以进一步提供功能定制

封装的控件,或者demo都是没有样式的,所以看着会比较丑一些,不过加样式也是分分钟。。。这里咱可以先看功能,需要即可定制

本篇文章我们首先介绍的就是股票,该控件支持常用的股票检索功能,支持模糊匹配,键盘上下键切换当前检索项等

右键菜单包括复制、粘贴、剪贴、全选等

本篇文章中不包括的功能也可以提供定制,需求合理即可。

下面来具体说一说这个功能的实现思路,会公开大多数核心代码,有需要的同学可以根据思路自行完善整个代码。

二、效果展示

如下效果图所示,是自选股使用上的一个展示效果,具有如下功能

  1. 搜索编辑框,支持股票代码和股票名称搜索
  2. 搜索预览框支持鼠标hover,并且可以使用键盘上下键进行当前项切换,单机时支持切换自选股
  3. 自选股列表,支持拖拽,拖拽时会有拖拽项映像,并示意将要拖拽到哪个位置
  4. 支持右键菜单,可以对某一项进行移动,删除等操作

如果觉着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("搜索股票代码/名称"));

由于篇幅原因,本篇文章就只先说搜索编辑框吧,本来想把自选股列表页一起加上,不过觉着内容太多,也不利于大家吸收,下一篇文章补上吧。。。

写的手都酸了,其他内容自行脑补吧。。。

四、相关文章

财联社-产品展示

Qt之自定义QLineEdit右键菜单

qt捕获全局windows消息

高仿富途牛牛-组件化(一)-支持页签拖拽、增删、小工具

高仿富途牛牛-组件化(二)-磁力吸附

高仿富途牛牛-组件化(三)-界面美化

高仿富途牛牛-组件化(四)-优秀的时钟

高仿富途牛牛-组件化(五)-如何去管理炒鸡多的小窗口

高仿富途牛牛-组件化(六)-炒鸡牛逼的布局记忆功能(序列化和反序列化)


如果您觉得文章不错,不妨给个打赏,写作不易,感谢各位的支持。您的支持是我最大的动力,谢谢!!!




很重要--转载声明

  1. 本站文章无特别说明,皆为原创,版权所有,转载时请用链接的方式,给出原文出处。同时写上原作者:朝十晚八 or Twowords

  2. 如要转载,请原文转载,如在转载时修改本文,请事先告知,谢绝在转载时通过修改本文达到有利于转载者的目的。


posted @ 2019-07-08 23:40  朝十晚八  阅读(2839)  评论(5编辑  收藏  举报

返回顶部