1 需求描述
- 實現一個Qt無邊框窗口,自定義最大化、最小化、關閉按鈕;
- 窗口支持任意拉伸、移動,支持邊框陰影;
- 窗口能夠集成任意其它窗口到內部形成一個整體。
2 設計思路
最初實現無邊框的目標只有一個,即簡單好用。所有實現基於Qt本身,現將窗口分為三層,如圖:
外層和內容層均使用垂直布局,使窗口拉伸時能夠自動適應大小。下面對每一層做個簡單說明。
2.1 XWidget
作為窗口的最外層,設置為透明,為內層ContentWidget邊框設置、陰影顯示提供支持。同時根據位置設置光標形狀(CursorShape),實現窗口的任意拉伸。
2.2 ContentWidget
作為內容包含層,可設置邊框顏色、寬度、圓角、陰影等效果,同時增加最大化、最小化、關閉按鈕,以及logo、軟件名稱顯示部件。
2.3 CentralWidget
作為外部嵌入層,XWidget提供一個接口void setCentralWidget(QWidget *widget)
,將其它窗口集成到ContentWidget內部形成一個整體,這個與QMainWindow類似。
3 代碼實現
- 首先,隱藏標題欄、啟用樣式表,XWidget背景透明,代碼如下:
setWindowFlags(Qt::FramelessWindowHint); //隱藏標題欄(無邊框)
setAttribute(Qt::WA_StyledBackground); //啟用樣式背景繪制
setAttribute(Qt::WA_TranslucentBackground); //背景透明
- 為了實現鼠標的位置信息獲取不受子控件的影響,啟動鼠標懸浮追蹤,代碼如下:
setAttribute(Qt::WA_Hover);
- 隨后便可以在event事件處理函數中獲取到懸浮事件,將其轉換為鼠標移動事件進行統一處理,代碼如下:
bool XWidget::event(QEvent *event)
{
if (event->type() == QEvent::HoverMove) {
QHoverEvent *hoverEvent = static_cast<QHoverEvent *>(event);
QMouseEvent mouseEvent(QEvent::MouseMove, hoverEvent->pos(),
Qt::NoButton, Qt::NoButton, Qt::NoModifier);
mouseMoveEvent(&mouseEvent);
}
return QWidget::event(event);
}
- 進入鼠標移動事件,根據坐標設置鼠標對應的形狀,如果鼠標為按下狀態且到達XWidget邊界則拉伸窗口,否則只移動窗口,代碼如下:
m_bIsPressed 是否按下鼠標
m_bIsResizing 是否正在調整窗口,調整窗口大小時不移動窗口
void XWidget::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
m_bIsPressed = true;
m_pressPoint = event->globalPos();
}
return QWidget::mousePressEvent(event);
}
void XWidget::mouseMoveEvent(QMouseEvent *event)
{
if (m_bIsPressed) {
if (m_bIsResizing) {
m_movePoint = event->globalPos() - m_pressPoint;
m_pressPoint += m_movePoint;
} else {
if (!m_bIsDoublePressed && windowState() == Qt::WindowMaximized) {
restoreWidget();
QPointF point(width() * ((double)(event->globalPos().x())/QApplication::desktop()->width()),
height() * ((double)(event->globalPos().y())/QApplication::desktop()->height()));
move(event->globalPos() - point.toPoint());
m_pressPoint = event->globalPos();
}
QPoint point = event->globalPos() - m_pressPoint;
move(pos() + point);
m_pressPoint = event->globalPos();
}
}
if (windowState() != Qt::WindowMaximized) {
updateRegion(event);
}
QWidget::mouseMoveEvent(event);
}
void XWidget::updateRegion(QMouseEvent *event)
{
QRect mainRect = geometry();
int marginTop = event->globalY() - mainRect.y();
int marginBottom = mainRect.y() + mainRect.height() - event->globalY();
int marginLeft = event->globalX() - mainRect.x();
int marginRight = mainRect.x() + mainRect.width() - event->globalX();
if (!m_bIsResizing) {
if ( (marginRight >= MARGIN_MIN_SIZE && marginRight <= MARGIN_MAX_SIZE)
&& ((marginBottom <= MARGIN_MAX_SIZE) && marginBottom >= MARGIN_MIN_SIZE) ) {
m_direction = BOTTOMRIGHT;
setCursor(Qt::SizeFDiagCursor);
} else if ( (marginTop >= MARGIN_MIN_SIZE && marginTop <= MARGIN_MAX_SIZE)
&& (marginRight >= MARGIN_MIN_SIZE && marginRight <= MARGIN_MAX_SIZE)) {
m_direction = TOPRIGHT;
setCursor(Qt::SizeBDiagCursor);
} else if ( (marginLeft >= MARGIN_MIN_SIZE && marginLeft <= MARGIN_MAX_SIZE)
&& (marginTop >= MARGIN_MIN_SIZE && marginTop <= MARGIN_MAX_SIZE) ) {
m_direction = TOPLEFT;
setCursor(Qt::SizeFDiagCursor);
} else if ( (marginLeft >= MARGIN_MIN_SIZE && marginLeft <= MARGIN_MAX_SIZE)
&& (marginBottom >= MARGIN_MIN_SIZE && marginBottom <= MARGIN_MAX_SIZE)) {
m_direction = BOTTOMLEFT;
setCursor(Qt::SizeBDiagCursor);
} else if (marginBottom <= MARGIN_MAX_SIZE && marginBottom >= MARGIN_MIN_SIZE) {
m_direction = DOWN;
setCursor(Qt::SizeVerCursor);
} else if (marginLeft <= MARGIN_MAX_SIZE - 1 && marginLeft >= MARGIN_MIN_SIZE - 1) {
m_direction = LEFT;
setCursor(Qt::SizeHorCursor);
} else if (marginRight <= MARGIN_MAX_SIZE && marginRight >= MARGIN_MIN_SIZE) {
m_direction = RIGHT;
setCursor(Qt::SizeHorCursor);
} else if (marginTop <= MARGIN_MAX_SIZE && marginTop >= MARGIN_MIN_SIZE) {
m_direction = UP;
setCursor(Qt::SizeVerCursor);
} else {
if (!m_bIsPressed) {
setCursor(Qt::ArrowCursor);
}
}
}
if (NONE != m_direction) {
m_bIsResizing = true;
resizeRegion(marginTop, marginBottom, marginLeft, marginRight);
}
}
不要看着代碼多就感覺復雜,上面其實就干了一件事,判斷鼠標是否達到邊框限定位置,達到了就把方向記錄下來。
void XWidget::resizeRegion(int marginTop, int marginBottom,
int marginLeft, int marginRight)
{
if (m_bIsPressed) {
switch (m_direction) {
case BOTTOMRIGHT:
{
QRect rect = geometry();
rect.setBottomRight(rect.bottomRight() + m_movePoint);
setGeometry(rect);
}
break;
case TOPRIGHT:
{
if (marginLeft > minimumWidth() && marginBottom > minimumHeight()) {
QRect rect = geometry();
rect.setTopRight(rect.topRight() + m_movePoint);
setGeometry(rect);
}
}
break;
case TOPLEFT:
{
if (marginRight > minimumWidth() && marginBottom > minimumHeight()) {
QRect rect = geometry();
rect.setTopLeft(rect.topLeft() + m_movePoint);
setGeometry(rect);
}
}
break;
case BOTTOMLEFT:
{
if (marginRight > minimumWidth() && marginTop> minimumHeight()) {
QRect rect = geometry();
rect.setBottomLeft(rect.bottomLeft() + m_movePoint);
setGeometry(rect);
}
}
break;
case RIGHT:
{
QRect rect = geometry();
rect.setWidth(rect.width() + m_movePoint.x());
setGeometry(rect);
}
break;
case DOWN:
{
QRect rect = geometry();
rect.setHeight(rect.height() + m_movePoint.y());
setGeometry(rect);
}
break;
case LEFT:
{
if (marginRight > minimumWidth()) {
QRect rect = geometry();
rect.setLeft(rect.x() + m_movePoint.x());
setGeometry(rect);
}
}
break;
case UP:
{
if (marginBottom > minimumHeight()) {
QRect rect = geometry();
rect.setTop(rect.y() + m_movePoint.y());
setGeometry(rect);
}
}
break;
default:
{
}
break;
}
} else {
m_bIsResizing = false;
m_direction = NONE;
}
}
同樣的,上面這段代碼也只干了一件事,如果鼠標達到了邊框限定位置,並且按下了鼠標按鍵,就跟着改變窗口大小。
- 對標記成員進行重置,代碼如下:
void XWidget::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
m_bIsPressed = false;
m_bIsResizing = false;
m_bIsDoublePressed = false;
}
QWidget::mouseReleaseEvent(event);
}
void XWidget::leaveEvent(QEvent *event)
{
m_bIsPressed = false;
m_bIsDoublePressed = false;
m_bIsResizing = false;
QWidget::leaveEvent(event);
}
- 最后實現ContentWidget邊框陰影效果,代碼如下:
void XWidget::createShadow()
{
QGraphicsDropShadowEffect *shadowEffect = new QGraphicsDropShadowEffect(this);
shadowEffect->setColor(Qt::black);
shadowEffect->setOffset(0, 0);
shadowEffect->setBlurRadius(13);
ui->widgetContent->setGraphicsEffect(shadowEffect);
}
此方法雖有效,會損耗性能,復雜界面不建議使用。
- 由於ContentWidget和XWidget之間有間距,最大化時可能不能占滿全屏,手動處理下,最大化時邊距設為0,還原時恢復即可,代碼如下:
void XWidget::maximizeWidget()
{
ui->pushButtonRestore->show();
ui->pushButtonMax->hide();
ui->verticalLayoutShadow->setContentsMargins(0, 0, 0, 0);
showMaximized();
}
void XWidget::restoreWidget()
{
ui->pushButtonRestore->hide();
ui->pushButtonMax->show();
ui->verticalLayoutShadow->setContentsMargins(9, 9, 9, 9);
showNormal();
}
4 QSS一下
#widgetContent {
background-color: white;
border: 1px solid lightgray;
border-radius: 3px;
}
#widgetContent QTreeWidget {
border: 1px solid lightgray;
}
#titleBarWidget QPushButton {
min-width: 25px;
max-width: 25px;
min-height: 25px;
max-height: 25px;
qproperty-flat: true;
border: none;
}
#titleBarWidget QPushButton:hover {
background-color: #D5E1F2;
}
#titleBarWidget QPushButton:pressed {
background-color: #A3BDE3;
}
#titleBarWidget QPushButton#pushButtonClose {
border-image: url(:/img/titleBar/close.png) 0 0 0 0 stretch stretch;
}
#titleBarWidget QPushButton#pushButtonRestore {
border-image: url(:/img/titleBar/restore.png) 0 0 0 0 stretch stretch;
}
#titleBarWidget QPushButton#pushButtonMax {
border-image: url(:/img/titleBar/max.png) 0 0 0 0 stretch stretch;
}
#titleBarWidget QPushButton#pushButtonMin {
border-image: url(:/img/titleBar/min.png) 0 0 0 0 stretch stretch;
}
#titleBarWidget QPushButton#pushButtonMenu {
border-image: url(:/img/titleBar/menu.png) 0 0 0 0 stretch stretch;
}
#menuBarTabWidget::tab-bar {
left: 65px;
}
#menuBarTabWidget {
border: 1px;
}
#menuBarTabWidget {
background-color: #2B579A;
}
#menuBarTabWidget::pane {
border: 1px solid lightgray;
border-left: 0px;
border-right: 0px;
}
#menuBarTabWidget QTabBar::tab{
min-width: 55px;
max-width: 55px;
min-height: 23px;
max-height: 23px;
}
#menuBarTabWidget QTabBar::tab {
background: transparent;
margin-left: 4px;
margin-right: 4px;
}
#menuBarTabWidget QTabBar::tab:hover {
color: #2B579A;
}
#menuBarTabWidget QTabBar::tab:selected {
border: 1px solid #BAC9DB;
background: white;
border-bottom-color: #FFFFFF;
}
#menuBarTabWidget QTabBar::tab:!selected {
margin-top: 1px;
}
QMenu {
background-color: #FCFCFC;
border: 1px solid #8492A6;
}
QMenu::item {
background-color: transparent;
}
QMenu::item:selected {
color: black;
background-color: #D5E1F2;
}
#pushBtnFileMenu {
min-width: 58px;
max-width: 58px;
min-height: 23px;
max-height: 23px;
color: white;
border: 1px solid #2B579A;
background-color: #2B579A;
}
#pushBtnFileMenu::menu-indicator {
image: none;
}
5 總結
之前也看了不少Qt實現FramelessWindow的例子,不是很復雜就是不通用。通過上面的實現,現在已完成了一個通用的版本,只要將自己的窗口設置到ContentWidget即可。本次實踐關鍵地方有以下三點:
- 界面的分層,感興趣的朋友可以嘗試下,如果沒有XWidget這一層會有什么效果,ContentWidget邊框效果會失效,這樣當然就達不到預期結果了;
- 啟用了WA_Hover鼠標懸浮追蹤,如果不啟用,鼠標的移動事件可能會被子控件覆蓋,這樣就不會知道鼠標是否到達邊框位置,從而無法正確設置鼠標的形狀;
- 窗口拉伸時有個偏移量m_movePoint,鼠標其實到達ContentWidget邊界就改變形狀了,拉伸是對XWidget進行的,所以這里有一定的偏移。
可能算不上最佳實踐,但是已經能夠滿足絕大多數使用場景了,往里面套就行,使用起來非常之簡單,還是很nice的。