一、概述
最近一直在仿照富途牛牛做組件化功能,目前已經有了初步的效果。
組件化基礎的功能已經有了,接下來就是一些細節上的處理了,比如說加載模板、保存模板、標簽頁修改名稱等等,細節上的問題我們在后續的文章中都會一一做以介紹
最近打算把組件化中的工具箱相關功能做以實現。比如說迷你報價、自選股、小時鍾這些窗口。
仔細觀察了牛牛的小窗口,無非就是一個窗口外殼,標題欄和客戶區內容,下面我們來具體分析下
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這個屬性,這樣我們的程序看起來就會更舒暢一些,不會出現很明顯的鋸齒
在縮放場景的時候,我們是這么干的
- 先把中心點平移到窗口中心
- 使用縮放比例進行縮放場景
- 計算我們字體的寬度和高度
- 定義我們字體需要繪制的矩形
- 在矩形中繪制我們要顯示的文本
這里需要注意一個點,我們必須要計算矩形來繪制文字,如果想計算文字的起始點坐標這個比較困難,因為文字繪制時,並不是說你給的起始點就是文本串的左上角
最后我們做一個定時器,每個一秒進行數據更新,然后刷新界面即可
//啟動定時器
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
-
如要轉載,請原文轉載,如在轉載時修改本文,請事先告知,謝絕在轉載時通過修改本文達到有利於轉載者的目的。