This commit is contained in:
zhuzihcu
2023-03-02 18:21:43 +08:00
parent 91be0e4da2
commit 1b5223a7d5
23 changed files with 746 additions and 361 deletions

View File

@ -1,93 +1,17 @@
#include "FramelessView.h"
#include <QGuiApplication>
#include <QQuickItem>
#include <QScreen>
#include <QWindow>
#include <VersionHelpers.h>
#include <WinUser.h>
#include <dwmapi.h>
#include <objidl.h> // Fixes error C2504: 'IUnknown' : base class undefined
#include <windows.h>
#include <windowsx.h>
#include <wtypes.h>
#pragma comment(lib, "Dwmapi.lib") // Adds missing library, fixes error LNK2019: unresolved
#pragma comment(lib, "User32.lib")
#pragma comment(lib, "Gdi32.lib")
// we cannot just use WS_POPUP style
// WS_THICKFRAME: without this the window cannot be resized and so aero snap, de-maximizing and minimizing won't work
// WS_SYSMENU: enables the context menu with the move, close, maximize, minize... commands (shift + right-click on the task bar item)
// WS_CAPTION: enables aero minimize animation/transition
// WS_MAXIMIZEBOX, WS_MINIMIZEBOX: enable minimize/maximize
QMap<WId,FramelessView*>* FramelessView::windowCache = new QMap<WId,FramelessView*>;
enum class Style : DWORD
class FramelessViewPrivate
{
windowed = WS_OVERLAPPEDWINDOW | WS_THICKFRAME | WS_CAPTION | WS_SYSMENU | WS_MAXIMIZEBOX | WS_MINIMIZEBOX,
aero_borderless = WS_POPUP | WS_THICKFRAME | WS_CAPTION | WS_SYSMENU | WS_MAXIMIZEBOX | WS_MINIMIZEBOX,
basic_borderless = WS_POPUP | WS_THICKFRAME | WS_SYSMENU | WS_MAXIMIZEBOX | WS_MINIMIZEBOX
public:
bool m_isMax = false;
bool m_isFull = false;
bool m_deleteLater = false;
QQuickItem *m_titleItem = nullptr;
};
static bool isCompositionEnabled()
{
BOOL composition_enabled = FALSE;
bool success = ::DwmIsCompositionEnabled(&composition_enabled) == S_OK;
return composition_enabled && success;
}
static Style selectBorderLessStyle()
{
return isCompositionEnabled() ? Style::aero_borderless : Style::basic_borderless;
}
static void setShadow(HWND handle, bool enabled)
{
if (isCompositionEnabled())
{
static const MARGINS shadow_state[2] { { 0, 0, 0, 0 }, { 1, 1, 1, 1 } };
::DwmExtendFrameIntoClientArea(handle, &shadow_state[enabled]);
}
}
static long hitTest(RECT winrect, long x, long y, int borderWidth)
{
// 鼠标区域位于窗体边框,进行缩放
if ((x >= winrect.left) && (x < winrect.left + borderWidth) && (y >= winrect.top) && (y < winrect.top + borderWidth))
{
return HTTOPLEFT;
}
else if (x < winrect.right && x >= winrect.right - borderWidth && y >= winrect.top && y < winrect.top + borderWidth)
{
return HTTOPRIGHT;
}
else if (x >= winrect.left && x < winrect.left + borderWidth && y < winrect.bottom && y >= winrect.bottom - borderWidth)
{
return HTBOTTOMLEFT;
}
else if (x < winrect.right && x >= winrect.right - borderWidth && y < winrect.bottom && y >= winrect.bottom - borderWidth)
{
return HTBOTTOMRIGHT;
}
else if (x >= winrect.left && x < winrect.left + borderWidth)
{
return HTLEFT;
}
else if (x < winrect.right && x >= winrect.right - borderWidth)
{
return HTRIGHT;
}
else if (y >= winrect.top && y < winrect.top + borderWidth)
{
return HTTOP;
}
else if (y < winrect.bottom && y >= winrect.bottom - borderWidth)
{
return HTBOTTOM;
}
else
{
return 0;
}
}
static bool isMaxWin(QWindow* win)
{
@ -98,53 +22,10 @@ static bool isFullWin(QQuickView* win)
return win->windowState() == Qt::WindowFullScreen;
}
class FramelessViewPrivate
{
public:
bool m_firstRun = true;
bool m_isMax = false;
bool m_isFull = false;
bool m_deleteLater = false;
QQuickItem* m_titleItem = nullptr;
HMENU mMenuHandler = NULL;
bool borderless = true; // is the window currently borderless
bool borderless_resize = true; // should the window allow resizing by dragging the borders while borderless
bool borderless_drag = true; // should the window allow moving my dragging the client area
bool borderless_shadow = true; // should the window display a native aero shadow while borderless
void setBorderLess(HWND handle, bool enabled)
{
auto newStyle = enabled ? selectBorderLessStyle() : Style::windowed;
auto oldStyle = static_cast<Style>(::GetWindowLongPtrW(handle, GWL_STYLE));
if (oldStyle != newStyle)
{
borderless = enabled;
//todo 有待研究这个
// ::SetWindowLongPtrW(handle, GWL_STYLE, static_cast<LONG>(newStyle));
// when switching between borderless and windowed, restore appropriate shadow state
setShadow(handle, borderless_shadow && (newStyle != Style::windowed));
// redraw frame
::SetWindowPos(handle, nullptr, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE);
::ShowWindow(handle, SW_SHOW);
}
}
void setBorderLessShadow(HWND handle, bool enabled)
{
if (borderless)
{
borderless_shadow = enabled;
setShadow(handle, enabled);
}
}
};
FramelessView::FramelessView(QWindow* parent)
: QQuickView(parent)
, d(new FramelessViewPrivate)
FramelessView::FramelessView(QWindow *parent) : Super(parent), d(new FramelessViewPrivate)
{
//此处不需要设置flags
// setFlags(Qt::CustomizeWindowHint | Qt::Window | Qt::FramelessWindowHint | Qt::WindowMinMaxButtonsHint | Qt::WindowTitleHint |
// Qt::WindowSystemMenuHint);
setFlags(Qt::CustomizeWindowHint | Qt::Window | Qt::FramelessWindowHint | Qt::WindowMinMaxButtonsHint | Qt::WindowTitleHint | Qt::WindowSystemMenuHint);
setResizeMode(SizeRootObjectToView);
setIsMax(windowState() == Qt::WindowMaximized);
@ -154,79 +35,26 @@ FramelessView::FramelessView(QWindow* parent)
setIsMax(windowState() == Qt::WindowMaximized);
setIsFull(windowState() == Qt::WindowFullScreen);
});
QObject::connect(this, &QQuickView::statusChanged, this, [&](QQuickView::Status status) {
if (status == QQuickView::Status::Ready) {
FramelessView::windowCache->insert(this->winId(),this);
}
});
}
void FramelessView::showEvent(QShowEvent* e)
{
if (d->m_firstRun)
{
d->m_firstRun = false;
//第一次show的时候设置无边框。不在构造函数中设置。取winId会触发QWindowsWindow::create,直接创建win32窗口,引起错乱(win7 或者虚拟机启动即黑屏)。
d->setBorderLess((HWND)(winId()), d->borderless);
{
// Qt 5.15.2 的bug; 问题复现及解决方法当使用WM_NCCALCSIZE 修改非客户区大小后移动窗口到其他屏幕时qwindows.dll 源码 qwindowswindow.cpp:2447
// updateFullFrameMargins() 函数 处会调用qwindowswindow.cpp:2453 的
// calculateFullFrameMargins函数重新获取默认的非客户区大小导致最外层窗口移动屏幕时会触发resize消息引起40像素左右的黑边故此处创建Menu
// 使其调用qwindowswindow.cpp:2451 的 QWindowsContext::forceNcCalcSize() 函数计算非客户区大小
//已知负面效果: 引入win32 MENU后Qt程序中如果有alt开头的快捷键会不生效被Qt滤掉了需要修改Qt源码
// QWindowsKeyMapper::translateKeyEventInternal 中的
// if (msgType == WM_SYSKEYDOWN && (nModifiers & AltAny) != 0 && GetMenu(msg.hwnd) != nullptr)
// return false;
// 这两行屏蔽掉
d->mMenuHandler = ::CreateMenu();
::SetMenu((HWND)winId(), d->mMenuHandler);
}
}
Super::showEvent(e);
}
FramelessView::~FramelessView()
{
if (d->mMenuHandler != NULL)
{
::DestroyMenu(d->mMenuHandler);
}
FramelessView::windowCache->remove(this->winId());
delete d;
}
bool FramelessView::isMax() const
void FramelessView::showEvent(QShowEvent *e)
{
return d->m_isMax;
Super::showEvent(e);
}
bool FramelessView::isFull() const
{
return d->m_isFull;
}
QQuickItem* FramelessView::titleItem() const
{
return d->m_titleItem;
}
void FramelessView::setTitleItem(QQuickItem* item)
{
d->m_titleItem = item;
}
QRect FramelessView::calcCenterGeo(const QRect& screenGeo, const QSize& normalSize)
QRect FramelessView::calcCenterGeo(const QRect &screenGeo, const QSize &normalSize)
{
int w = normalSize.width();
int h = normalSize.height();
int x = screenGeo.x() + (screenGeo.width() - w) / 2;
int y = screenGeo.y() + (screenGeo.height() - h) / 2;
if (screenGeo.width() < w)
{
if (screenGeo.width() < w) {
x = screenGeo.x();
w = screenGeo.width();
}
if (screenGeo.height() < h)
{
if (screenGeo.height() < h) {
y = screenGeo.y();
h = screenGeo.height();
}
@ -236,18 +64,28 @@ QRect FramelessView::calcCenterGeo(const QRect& screenGeo, const QSize& normalSi
void FramelessView::moveToScreenCenter()
{
auto geo = calcCenterGeo(screen()->availableGeometry(), size());
if (minimumWidth() > geo.width() || minimumHeight() > geo.height())
{
if (minimumWidth() > geo.width() || minimumHeight() > geo.height()) {
setMinimumSize(geo.size());
}
setGeometry(geo);
update();
}
void FramelessView::closeDeleteLater(){
d->m_deleteLater = true;
}
bool FramelessView::isMax() const
{
return d->m_isMax;
}
bool FramelessView::isFull() const
{
return d->m_isFull;
}
QQuickItem *FramelessView::titleItem() const
{
return d->m_titleItem;
}
void FramelessView::setIsMax(bool isMax)
{
if (d->m_isMax == isMax)
@ -258,56 +96,43 @@ void FramelessView::setIsMax(bool isMax)
}
void FramelessView::setIsFull(bool isFull)
{
if (d->m_isFull == isFull)
if(d->m_isFull == isFull)
return;
d->m_isFull = isFull;
emit isFullChanged(d->m_isFull);
}
void FramelessView::resizeEvent(QResizeEvent* e)
void FramelessView::setTitleItem(QQuickItem *item)
{
// SetWindowRgn(HWND(winId()), CreateRoundRectRgn(0, 0, width(), height(), 4, 4), true);
Super::resizeEvent(e);
d->m_titleItem = item;
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
bool FramelessView::nativeEvent(const QByteArray& eventType, void* message, qintptr* result)
bool FramelessView::nativeEvent(const QByteArray &eventType, void *message, qintptr *result)
#else
bool FramelessView::nativeEvent(const QByteArray& eventType, void* message, long* result)
bool FramelessView::nativeEvent(const QByteArray &eventType, void *message, long *result)
#endif
{
const long border_width = 4;
if (!result)
{
//防御式编程
//一般不会发生这种情况win7一些极端情况会传空指针进来。解决方案是升级驱动、切换到basic主题。
return false;
}
#if (QT_VERSION == QT_VERSION_CHECK(5, 11, 1))
// Work-around a bug caused by typo which only exists in Qt 5.11.1
const auto msg = *reinterpret_cast<MSG**>(message);
#else
const auto msg = static_cast<LPMSG>(message);
#endif
if (!msg || !msg->hwnd)
{
return false;
}
switch (msg->message)
{
case WM_CLOSE:{
if(d->m_deleteLater){
deleteLater();
}
}
case WM_NCCALCSIZE: {
#if 1
const auto mode = static_cast<BOOL>(msg->wParam);
const auto clientRect = mode ? &(reinterpret_cast<LPNCCALCSIZE_PARAMS>(msg->lParam)->rgrc[0]) : reinterpret_cast<LPRECT>(msg->lParam);
if (mode == TRUE && d->borderless)
if (mode == TRUE)
{
*result = WVR_REDRAW;
//规避 拖动border进行resize时界面闪烁
@ -333,51 +158,11 @@ bool FramelessView::nativeEvent(const QByteArray& eventType, void* message, long
#endif
break;
}
case WM_NCACTIVATE: {
if (!isCompositionEnabled())
{
// Prevents window frame reappearing on window activation
// in "basic" theme, where no aero shadow is present.
*result = 1;
return true;
}
break;
}
case WM_NCHITTEST: {
if (d->borderless)
{
RECT winrect;
GetWindowRect(HWND(winId()), &winrect);
long x = GET_X_LPARAM(msg->lParam);
long y = GET_Y_LPARAM(msg->lParam);
*result = 0;
if (!isMaxWin(this) && !isFullWin(this))
{ //非最大化、非全屏时,进行命中测试,处理边框拖拽
if(!((maximumHeight()==minimumHeight())&&(maximumWidth()==minimumWidth()))){
*result = hitTest(winrect, x, y, border_width);
if (0 != *result)
{
return true;
}
}
}
if (d->m_titleItem)
{
auto titlePos = d->m_titleItem->mapToGlobal({ 0, 0 });
titlePos = mapFromGlobal(titlePos.toPoint());
auto titleRect = QRect(titlePos.x(), titlePos.y(), d->m_titleItem->width(), d->m_titleItem->height());
double dpr = qApp->devicePixelRatio();
QPoint pos = mapFromGlobal(QPoint(x / dpr, y / dpr));
if (titleRect.contains(pos))
{
*result = HTCAPTION;
return true;
}
}
return false;
}
break;
} // end case WM_NCHITTEST
}
return Super::nativeEvent(eventType, message, result);
}
void FramelessView::resizeEvent(QResizeEvent *e)
{
Super::resizeEvent(e);
}