Qt之股票元件-股票檢索--支援搜尋結果預覽、滑鼠、鍵盤操作

朝十晚八發表於2019-07-08

原文連結:Qt之股票元件-股票檢索--支援搜尋結果預覽、滑鼠、鍵盤操作

一、感慨一下

之前做過一款炒股軟體,個人覺著是我職業生涯裡做過的效果最好的一款產品,而且速度也不慢,效果可以參考財聯社-產品展示這篇文章,當然這篇文章只能顯示有限的內容,其中整個程式碼的結構、一些好的方法和設計模式是沒有機會展示的。

最近聽到一個不好的訊息,我們的產品夭折了。剛聽到這個訊息時心理還挺不是滋味的,畢竟這個產品我是從頭參與到尾,後來因為種種原因離開了,產品功能也就此終結,但回想起那段開發的日子,真的是收穫滿滿。更確切的說,這個產品應該是換了一種語言重新開始做。

不爽歸不爽,可整個產品的程式碼還是不錯的,因此 後續有時間我會慢慢的把一些好的程式碼抽離出來,編譯成一個個可以單獨執行的demo,方便有需要的朋友使用。

如果有需要的朋友可以加我好友,有償提供原始碼、或者也可以進一步提供功能定製

封裝的控制元件,或者demo都是沒有樣式的,所以看著會比較醜一些,不過加樣式也是分分鐘。。。這裡我們可以先看功能,需要即可定製

本篇文章我們首先介紹的就是股票,該控制元件支援常用的股票檢索功能,支援模糊匹配,鍵盤上下鍵切換當前檢索項等

右鍵選單包括複製、貼上、剪貼、全選等

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

下面來具體說一說這個功能的實現思路,會公開大多數核心程式碼,有需要的同學可以根據思路自行完善整個程式碼。

二、效果展示

如下效果圖所示,是自選股使用上的一個展示效果,具有如下功能

  1. 搜尋編輯框,支援股票程式碼和股票名稱搜尋
  2. 搜尋預覽框支援滑鼠hover,並且可以使用鍵盤上下鍵進行當前項切換,單機時支援切換自選股
  3. 自選股列表,支援拖拽,拖拽時會有拖拽項映像,並示意將要拖拽到哪個位置
  4. 支援右鍵選單,可以對某一項進行移動,刪除等操作
Qt之股票元件-股票檢索--支援搜尋結果預覽、滑鼠、鍵盤操作

如果覺著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訊息

高仿富途牛牛-元件化(一)-支援頁籤拖拽、增刪、小工具

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

高仿富途牛牛-元件化(三)-介面美化

高仿富途牛牛-元件化(四)-優秀的時鐘

高仿富途牛牛-元件化(五)-如何去管理炒雞多的小視窗

高仿富途牛牛-元件化(六)-炒雞牛逼的佈局記憶功能(序列化和反序列化)





如果您覺得文章不錯,不妨給個打賞,寫作不易,感謝各位的支援。您的支援是我最大的動力,謝謝!!!








Qt之股票元件-股票檢索--支援搜尋結果預覽、滑鼠、鍵盤操作 Qt之股票元件-股票檢索--支援搜尋結果預覽、滑鼠、鍵盤操作






很重要--轉載宣告

  1. 本站文章無特別說明,皆為原創,版權所有,轉載時請用連結的方式,給出原文出處。同時寫上原作者:朝十晚八 or Twowords

  2. 如要轉載,請原文轉載,如在轉載時修改本文,請事先告知,謝絕在轉載時通過修改本文達到有利於轉載者的目的。


相關文章