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

朝十晚八發表於2019-06-19

一、概述

最近一直在仿照富途牛牛做元件化功能,目前已經有了初步的效果。

元件化基礎的功能已經有了,接下來就是一些細節上的處理了,比如說載入模板、儲存模板、標籤頁修改名稱等等,細節上的問題我們在後續的文章中都會一一做以介紹

最近打算把元件化中的工具箱相關功能做以實現。比如說迷你報價、自選股、小時鐘這些視窗。

仔細觀察了牛牛的小視窗,無非就是一個視窗外殼,標題欄和客戶區內容,下面我們來具體分析下

1、視窗外殼

例如效果展示中的gif圖,小時鐘就是一個小視窗,他的外殼對我們肉眼所見到的騎士就是外邊線、或者說是高亮選中時候的黃色邊框。

視窗外殼是我們進行縮放小視窗的利器,當我們滑鼠屬於這個外殼時,我們按下滑鼠就可以對其進行放大、縮小,如果把滑鼠放到四個角進行拖拽的時,你會驚奇的發現他還可以同時朝著兩個方向進行縮放

2、標題欄

windows原生的視窗是包含標題欄的,同樣他還為我們提供了很好的放大縮小、拖拽、高亮訊息通知等很好用的功能。但是windows原生的標題欄屬於非客戶端,我們是不能直接進行操作的,也就是說我們不能對其進行更多的定製化操作。

這下糟心了,windows原生標題欄用不了了

既然windows原生的標題欄我們用不了,那麼我們就只能隱藏原生的標題欄,然後自己去模擬一個新的標題欄,然後實現我們自己需要的所有標題欄該有的行為。

對於這樣的小視窗我們已經實現了,在我們之前的文章中也有提到,他就是SmallWidget,只是在這個版本的程式碼裡,我們才實現了放大、縮小等功能

3、客戶區

理解了上邊2個概念,客戶區利器起來就很簡單了,他就是我們可以操作的區域。

既然標題欄都已經被我們重寫了,那麼其實我們定製化後的SmallWidget視窗,標題欄也就是一個客戶區了。

如gif圖中所展示的,小時鐘視窗上的時間就是客戶區內容了。


瞭解了小視窗這個概念之後,來進入我們本篇文章的重點內容--優秀的時鐘

接下來我將會講述下我們這個小時鐘是怎麼完成的。

二、效果展示

下面gif圖所示,錄製的時間比較長,大家可以仔細看下,互動效果完全是參照富途牛牛做的,只是目前視為沒有接入正式資料。

如有問題,歡迎提出。感謝!!!

歡迎大家提出問題,互動、配色都可以

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

三、小視窗

寫這篇文章之前,一直想寫一篇小視窗管理的文章,主要是為了更好的通過工具箱來構造小視窗,讓使用者使用起來成本更低,無奈架構一直沒有寫好,因此這裡一直往後推。

寫這篇文章的時候,小視窗管理的程式碼結構已經在搭建了,因此展示的程式碼可能會暴露這一點,但是這不是我們這篇文章講解的核心,暫時不用關心。

小視窗還是那個視窗類,就像上一篇文章高仿富途牛牛-元件化(三)-介面美化中說的那樣,只是這一次我們又加了一些新的功能。

最主要的就是我們重寫了滑鼠3大事件,用於處理縮放效果

virtual void mousePressEvent(QMouseEvent * event) override;
virtual void mouseMoveEvent(QMouseEvent *) override;
virtual void mouseReleaseEvent(QMouseEvent * event) override;

這三個函式各司其職,分工協作,完成了縮放事件

  • mousePressEvent:記錄滑鼠按下時的一些狀態,比如滑鼠按下位置、滑鼠按下狀態
  • mouseMoveEvent:負責處理移動事件,判斷滑鼠是否在視窗邊緣,如果在的話,修改滑鼠狀態
  • mouseReleaseEvent:當滑鼠抬起時,恢復滑鼠按下時記錄的資訊

下面主要分析下第二個步驟,滑鼠移動事件處理,他的處理程式碼可能像下面這樣,分為兩個部分:修改滑鼠狀態和修改視窗大小

void SmallWidget::mouseMoveEvent(QMouseEvent * event)
{
    if (mLeftButtonPressed)
    {
        ResizeWidget(event);
    }
    else
    {
        UpdateCursorShape(event->pos());
    }

    __super::mouseMoveEvent(event);
}

1、修改滑鼠狀態

滑鼠移動時,當我們發現沒有按下滑鼠左鍵,這個時候我們就執行UpdateCursorShape方法,去判斷是否可以進行修改視窗大小,同時修改滑鼠狀態

當滑鼠不在視窗邊緣時,也即不滿足修改滑鼠狀態時,記得把滑鼠狀態還原

a、判斷是否滿足拖拽

當滑鼠處於視窗邊緣時,並且在一定的範圍內,就認為他進入了修改大小的準備狀態

如程式碼所示,我們首先判斷滑鼠滿足四個邊的那幾個邊的縮放,然後在繼續判斷是否是在某一個角上拖拽,最後一個變數onEdges標誌著是否有縮放狀態。

當有一個變數onEdges變為true時,我們就認為要進行縮放,然後下一步我們就可以進行修改滑鼠狀態

void SmallWidget::Recalculate(const QPoint& mousePos, const QRect& frameRect)
{
    int mouseX = mousePos.x();
    int mouseY = mousePos.y();

    int frameX = frameRect.x();
    int frameY = frameRect.y();

    int frameWidth = frameRect.width();
    int frameHeight = frameRect.height();

    onLeftEdge = mouseX >= frameX && mouseX <= frameX + mBorderWidth;//左
    onRightEdge = mouseX >= frameX + frameWidth - mBorderWidth && mouseX <= frameX + frameWidth;//右 
    onTopEdge = mouseY >= frameY && mouseY <= frameY + mBorderWidth;//上
    onBottomEdge = mouseY >= frameY + frameHeight - mBorderWidth && mouseY <= frameY + frameHeight;//下
    
    onTopLeftEdge = onTopEdge && onLeftEdge;
    onBottomLeftEdge = onBottomEdge && onLeftEdge;
    onTopRightEdge = onTopEdge && onRightEdge;
    onBottomRightEdge = onBottomEdge && onRightEdge;

    //only these checks would be enough
    onEdges = onLeftEdge || onRightEdge || onTopEdge || onBottomEdge;
}

b、修改滑鼠狀態

判斷完是否有邊可以進行縮放之後,我們只需要根據這些變數就可以輕易知道,我們要把滑鼠狀態改成什麼樣子了。

最後記住,如果不使用了,記得還原滑鼠狀態

void SmallWidget::UpdateCursorShape(const QPoint & mousePos)
{
    Recalculate(mousePos, rect());

    if (onTopLeftEdge || onBottomRightEdge)
    {
        setCursor(Qt::SizeFDiagCursor);
        mCursorShapeChanged = true;
    }
    else if (onTopRightEdge || onBottomLeftEdge)
    {
        setCursor(Qt::SizeBDiagCursor);
        mCursorShapeChanged = true;
    }
    else if (onLeftEdge || onRightEdge)
    {
        setCursor(Qt::SizeHorCursor);
        mCursorShapeChanged = true;
    }
    else if (onTopEdge || onBottomEdge)
    {
        setCursor(Qt::SizeVerCursor);
        mCursorShapeChanged = true;
    }
    else
    {
        if (mCursorShapeChanged)//修改滑鼠狀態
        {
            unsetCursor();
            mCursorShapeChanged = false;
        }
    }
}

2、修改大小

修改了滑鼠狀態實在滑鼠未按下的時候觸發的,一旦我們修改了滑鼠狀態後,拖拽的第一步準備工作算是做到位了,這個時候我們只要按下滑鼠,繼續移動滑鼠,然後就進入了修改視窗大小的流程中

修改視窗大小主要是使用了我們滑鼠按下時記錄的一些資訊

視窗大小的量應該等於滑鼠按下時到移動的距離偏移,這裡我們就那有邊框右移來說明問題

假設說右邊框本身的值時500,我們滑鼠按下時的全域性座標是1000,這個時候滑鼠向右移動,移動到了1100這個座標,那麼滑鼠其實就是移動了100畫素,那麼這樣就很清晰了,我們的右邊框此時的值應該是500+100=600。

為什麼移動後的位置 = 從按下時視窗的位置+滑鼠按下時到當前位置的偏移量?這樣做有一個很大的好處,那就是我們不需要考慮中間的過程,及時中間有些地方處理錯了,如果又一次處罰了縮放,那麼錯誤也會被修正。

還有一個好處就是,如果我們每次只做上一次和本次的滑鼠位置偏移量,這樣處理結果會有異常,根據機器效能,有些機器會丟失需要處理的事件,導致滑鼠移動的距離大,我們視窗縮放的少,這個主要是因為Qt把很多事件優化了。

void SmallWidget::ResizeWidget(QMouseEvent * event)
{
    QPoint mousePos = event->pos();
    QPoint globalPos = event->globalPos();

    QPoint offsetPos = globalPos - m_PressPos;

    QRect origRect = m_PressRect;

    if (onLeftEdge)
    {
        origRect.setLeft(origRect.left() + offsetPos.x());
    }
    else if (onRightEdge)
    {
        origRect.setRight(origRect.right() + offsetPos.x());
    }
    else if (onTopEdge)
    {
        origRect.setTop(origRect.top() + offsetPos.y());
    }
    else if (onBottomEdge)
    {
        origRect.setBottom(origRect.bottom() + offsetPos.y());
    }
    //其他四個角的處理省略
    if (onEdges)
    {
        move(origRect.topLeft());
        resize(origRect.size());
    }
}

四、時鐘視窗

講完了小視窗,我們的小時鐘已經具有了放大、縮小、和移動的功能。

接下來我們分析下這個時鐘是怎麼顯示的,貌似他好像還支援自提自動放大。

首先我們來分析下這個時鐘視窗的佈局,上邊是一個動態重新整理的時間文字,下邊是一個文字框,主要顯示當前日期。仔細看效果展示的gif圖,其中主要的應該是上半部分的時間了,因為他居然支援視窗當大時,自身也可以平滑的放大

整個視窗的程式碼佈局,我這裡就不做介紹了,比較簡單,就是一個垂直佈局,其中有一個需要注意的地方就是分割線,研究過QtDesigner的同學應該都知道,Qt中的分割線其實就是一個畫素的QFrame,他的實現程式碼可能像下面這樣,然後加入佈局即可

//實現程式碼
QFrame * line = new QFrame;
line->setObjectName("line");
line->setFixedHeight(1);

//樣式表
ClockSmall QFrame#line{border:1 solid #474F57;}

重要環節登場了,也是我們本篇文章中的核心,支援字型平滑放大,就像效果圖那樣

void TimeLabel::paintEvent(QPaintEvent * event)
{
    __super::paintEvent(event);

    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);

    QFont font = painter.font();
    font.setPixelSize(14);
    painter.setFont(font);

    double side = qMin(width(), height());

    int text_w = painter.fontMetrics().width(m_text);
    int text_h = painter.fontMetrics().height();
    
    painter.translate(width() / 2, height() / 2);
    painter.scale(side / text_w * 1.5, side / text_w * 1.5);

    QRect r(-text_w / 2, -text_h / 2, text_w, text_h);
    painter.drawText(r, m_text);
}

上面這幾行程式碼就是負責繪製時鐘的,這裡邊使用到了一個技巧--場景縮放

painter.scale()這行程式碼可以把繪製的場景縮放一個比例係數,也就是當我們的窗體放大縮小時,我們根據窗體的大小計算出一個合適的縮放比,然後把場景進行縮放,這樣我們的字型自然而然就會變大

繪製時,我們也應該開啟平滑繪製QPainter::Antialiasing這個屬性,這樣我們的程式看起來就會更舒暢一些,不會出現很明顯的鋸齒

在縮放場景的時候,我們是這麼幹的

  1. 先把中心點平移到視窗中心
  2. 使用縮放比例進行縮放場景
  3. 計算我們字型的寬度和高度
  4. 定義我們字型需要繪製的矩形
  5. 在矩形中繪製我們要顯示的文字

這裡需要注意一個點,我們必須要計算矩形來繪製文字,如果想計算文字的起始點座標這個比較困難,因為文字繪製時,並不是說你給的起始點就是文字串的左上角

最後我們做一個定時器,每個一秒進行資料更新,然後重新整理介面即可

//啟動定時器
QTimer * timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &ClockSmall::UpdateTime);
timer->start(1000);

UpdateTime();
    
//更新資料
void ClockSmall::UpdateTime()
{
    QDateTime dt = QDateTime::currentDateTime();
    m_pTime->SetText(dt.time().toString("HH:mm:ss"));

    QString text = QStringLiteral("北京(CN) ") + dt.date().toString("yyyy/MM/d");
    m_pText->setText(text);
}

//更新資料 併發起繪製請求
void TimeLabel::SetText(const QString & text){ m_text = text; update(); }

五、相關文章

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

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

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




轉載宣告:本站文章無特別說明,皆為原創,版權所有,轉載請註明:朝十晚八 or Twowords


相關文章