1 开发测试环境
IDE:VS2017
Qt:5.14.2 x64
操作系统:Win10、Win11
实现效果:
2 实现
网上搜索Qt窗口的无边框化实现方案有很多,但从实现的难易程度,以及效果上来说,最优
的还是通过Windows消息来实现,即重新实现Qt窗口的nativeEvent方法。该方案不仅实现了无边
框窗口,还保留了窗口的拖动、贴边等系统特性,以下为实现代码(以QWidget为例,
QMainWindow、QDialog可以平移过去):
#include "ui/frameless_widget.h"
#include <windows.h>
#include <windowsx.h>
#pragma comment (lib,"user32.lib")
FramelessWidget::FramelessWidget(QWidget *parent) : QWidget(parent)
, movale_(true)
, resizable_(true)
, border_width_(5)
, title_bar_(Q_NULLPTR)
, just_maximized_(false)
{
this->setWindowFlags(this->windowFlags() | Qt::FramelessWindowHint | Qt::WindowSystemMenuHint | Qt::WindowMinimizeButtonHint);
this->installEventFilter(this);
HWND hwnd = (HWND)this->winId();
DWORD style = ::GetWindowLong(hwnd, GWL_STYLE);
::SetWindowLong(hwnd, GWL_STYLE, style | WS_MAXIMIZEBOX | WS_THICKFRAME | WS_CAPTION);
}
FramelessWidget::~FramelessWidget()
{
}
void FramelessWidget::setTitleBar(QWidget *title_bar)
{
title_bar_ = title_bar;
title_bar_->installEventFilter(this);
}
void FramelessWidget::showEvent(QShowEvent *event)
{
this->setAttribute(Qt::WA_Mapped);
QWidget::showEvent(event);
}
bool FramelessWidget::eventFilter(QObject *watched, QEvent *event)
{
if (watched == this)
{
if (event->type() == QEvent::WindowStateChange)
{
if (this->windowState() == Qt::WindowNoState)
{
movale_ = true;
resizable_ = true;
}
else
{
movale_ = false;
resizable_ = false;
}
emit signalWindowStateChanged(!movale_);
}
}
return QWidget::eventFilter(watched, event);
}
bool FramelessWidget::nativeEvent(const QByteArray &event_type, void *message, long *result)
{
MSG *msg = (MSG *)message;
switch (msg->message)
{
case WM_NCCALCSIZE:
{
//this kills the window frame and title bar we added with WS_THICKFRAME and WS_CAPTION
*result = 0;
return true;
}
case WM_NCHITTEST:
{
*result = 0;
const LONG border_width = border_width_;
RECT winrect;
GetWindowRect(HWND(winId()), &winrect);
long x = GET_X_LPARAM(msg->lParam);
long y = GET_Y_LPARAM(msg->lParam);
bool resize_width = minimumWidth() != maximumWidth();
bool resize_height = minimumHeight() != maximumHeight();
if (resize_width)
{
//left border
if (x >= winrect.left && x < winrect.left + border_width)
{
*result = HTLEFT;
}
//right border
if (x < winrect.right && x >= winrect.right - border_width)
{
*result = HTRIGHT;
}
}
if (resize_height)
{
//bottom border
if (y < winrect.bottom && y >= winrect.bottom - border_width)
{
*result = HTBOTTOM;
}
//top border
if (y >= winrect.top && y < winrect.top + border_width)
{
*result = HTTOP;
}
}
if (resize_width && resize_height)
{
//bottom left corner
if (x >= winrect.left && x < winrect.left + border_width &&
y < winrect.bottom && y >= winrect.bottom - border_width)
{
*result = HTBOTTOMLEFT;
}
//bottom right corner
if (x < winrect.right && x >= winrect.right - border_width &&
y < winrect.bottom && y >= winrect.bottom - border_width)
{
*result = HTBOTTOMRIGHT;
}
//top left corner
if (x >= winrect.left && x < winrect.left + border_width &&
y >= winrect.top && y < winrect.top + border_width)
{
*result = HTTOPLEFT;
}
//top right corner
if (x < winrect.right && x >= winrect.right - border_width &&
y >= winrect.top && y < winrect.top + border_width)
{
*result = HTTOPRIGHT;
}
}
//*result still equals 0, that means the cursor locate OUTSIDE the frame area
//but it may locate in titlebar area
if (*result == 0)
{
if (!title_bar_) return false;
QPoint pos = title_bar_->mapFromGlobal(QPoint(x, y));
if (!title_bar_->rect().contains(pos)) return false;
QWidget* child = title_bar_->childAt(pos);
if (!child)
{
*result = HTCAPTION;
return true;
}
else {
/*if (_lpWhiteList.contains(child))
{
*result = HTCAPTION;
return true;
}*/
return false;
}
}
return true;
} //end case WM_NCHITTEST
case WM_GETMINMAXINFO:
{
if (::IsZoomed(msg->hwnd)) {
RECT frame = { 0, 0, 0, 0 };
AdjustWindowRectEx(&frame, WS_OVERLAPPEDWINDOW, FALSE, 0);
//record frame area data
frames_.setLeft(abs(frame.left));
frames_.setTop(abs(frame.bottom));
frames_.setRight(abs(frame.right));
frames_.setBottom(abs(frame.bottom));
QWidget::setContentsMargins(frames_.left() + margins_.left(), \
frames_.top() + margins_.top(), \
frames_.right() + margins_.right(), \
frames_.bottom() + margins_.bottom());
just_maximized_ = true;
}
else {
if (just_maximized_)
{
QWidget::setContentsMargins(margins_);
//after window back to normal size from maximized state
//a twinkle will happen, to avoid this twinkle
//repaint() is important used just before the window back to normal
repaint();
frames_ = QMargins();
just_maximized_ = false;
}
}
return false;
}
default:
return QWidget::nativeEvent(event_type, message, result);
}
}
3 扩展
以上,已经实现一个无边框窗口,可以拖动边框进行放大、缩小,以及通过点击任务栏标签
进行最小化和还原。如果想进一步实现窗口的拖动以及贴边特性,则需要实现一个窗口标题栏,并
通过setTitleBar进行设置。为了方便扩展,我们提供一个中间适配窗口,继承自
FramelessWidget,实现基础的标题栏,并对外提供设置标题、隐藏最小化、最大化按钮等接口,
当软件有多个无边框窗口需求时,可以继承适配窗口,并通过接口定制标题栏显示内容。
#include "ui/base_widget.h"
BaseWidget::BaseWidget(QString title, QWidget *parent) : FramelessWidget(parent)
, layout_(Q_NULLPTR)
, title_bar_(Q_NULLPTR)
, title_bar_layout_(Q_NULLPTR)
, sys_icon_(Q_NULLPTR)
, title_(Q_NULLPTR)
, right_layout1_(Q_NULLPTR)
, right_layout2_(Q_NULLPTR)
, minimize_button_(Q_NULLPTR)
, switch_button_(Q_NULLPTR)
, close_button_(Q_NULLPTR)
, content_widget_(Q_NULLPTR)
, status_bar_(Q_NULLPTR)
, message_(Q_NULLPTR)
{
initUI();
title_->setText(title);
this->setTitleBar(title_bar_);
connect(this, &BaseWidget::signalWindowStateChanged, this, &BaseWidget::slotWindowStateChanged);
connect(minimize_button_, &QToolButton::clicked, this, &BaseWidget::slotMinimizeButtonClicked);
connect(switch_button_, &QToolButton::clicked, this, &BaseWidget::slotMaximizeButtonClicked);
connect(close_button_, &QToolButton::clicked, this, &BaseWidget::slotCloseButtonClicked);
}
BaseWidget::~BaseWidget()
{
}
void BaseWidget::initUI()
{
// 0
layout_ = new QVBoxLayout;
layout_->setContentsMargins(0, 0, 0, 0);
layout_->setSpacing(0);
// 1
title_bar_ = new QWidget(this);
title_bar_->setObjectName(QString("objTitleBar"));
title_bar_->setFixedHeight(42);
title_bar_layout_ = new QHBoxLayout;
title_bar_layout_->setContentsMargins(5, 0, 0, 0);
title_bar_layout_->setSpacing(0);
sys_icon_ = new QLabel(title_bar_);
title_ = new QLabel(title_bar_);
title_->setObjectName(QString("objTitle"));
right_layout1_ = new QVBoxLayout;
right_layout1_->setContentsMargins(1, 1, 1, 1);
right_layout2_ = new QHBoxLayout;
right_layout2_->setContentsMargins(0, 0, 0, 0);
right_layout2_->setSpacing(5);
minimize_button_ = new QToolButton(title_bar_);
minimize_button_->setObjectName(QString("objMinimizeButton32x32"));
switch_button_ = new QToolButton(title_bar_);
switch_button_->setObjectName(QString("objRestoreButton32x32"));
close_button_ = new QToolButton(title_bar_);
close_button_->setObjectName(QString("objCloseButton32x32"));
right_layout2_->addWidget(minimize_button_);
right_layout2_->addWidget(switch_button_);
right_layout2_->addWidget(close_button_);
right_layout1_->addLayout(right_layout2_);
right_layout1_->addStretch();
title_bar_layout_->addWidget(sys_icon_);
title_bar_layout_->addStretch();
title_bar_layout_->addWidget(title_);
title_bar_layout_->addStretch();
title_bar_layout_->addLayout(right_layout1_);
title_bar_->setLayout(title_bar_layout_);
content_widget_ = new QWidget(this);
content_widget_->setObjectName("objContentWidget");
status_bar_ = new QStatusBar(this);
message_ = new QLabel(status_bar_);
message_->setMinimumSize(message_->sizeHint());
message_->setAlignment(Qt::AlignVCenter);
status_bar_->addWidget(message_);
layout_->addWidget(title_bar_);
layout_->addWidget(content_widget_);
layout_->addWidget(status_bar_);
status_bar_->setMaximumHeight(30);
//status_bar_->showMessage(QStringLiteral("123"));
this->setLayout(layout_);
}
void BaseWidget::slotWindowStateChanged(bool max)
{
if (max)
{
switch_button_->setObjectName(QString("objRestoreButton24x24"));
switch_button_->setStyle(switch_button_->style());
}
else
{
switch_button_->setObjectName(QString("objMaximizeButton24x24"));
switch_button_->setStyle(switch_button_->style());
}
}
void BaseWidget::slotMinimizeButtonClicked()
{
this->showMinimized();
}
void BaseWidget::slotMaximizeButtonClicked()
{
if (this->isMaximized())
{
this->showNormal();
}
else
{
this->showMaximized();
}
}
void BaseWidget::slotCloseButtonClicked()
{
this->close();
}
本文含有隐藏内容,请 开通VIP 后查看