Revert back to the original solution.

We will use the original solution in this branch.
The new Windows Terminal solution will be in another branch.

Partially reverts commit 01707907cd

Signed-off-by: Yuhang Zhao <2546789017@qq.com>
This commit is contained in:
Yuhang Zhao 2020-04-26 21:40:37 +08:00
parent 01707907cd
commit f0ef569b08
3 changed files with 250 additions and 154 deletions

View File

@ -1,12 +1,7 @@
#include <QApplication> #include <QApplication>
#include <QWidget> #include <QWidget>
#include <QWindow>
#ifdef Q_OS_WINDOWS
#include "winnativeeventfilter.h"
#else
#include "framelesshelper.h" #include "framelesshelper.h"
#endif
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
@ -25,19 +20,10 @@ int main(int argc, char *argv[]) {
QApplication application(argc, argv); QApplication application(argc, argv);
#ifdef Q_OS_WINDOWS
// Do this AFTER the Q(Gui)Application is constructed!
WinNativeEventFilter::install();
#else
FramelessHelper helper; FramelessHelper helper;
#endif
QWindow window;
QWidget widget; QWidget widget;
#ifndef Q_OS_WINDOWS
helper.setFramelessWindows({&widget}); helper.setFramelessWindows({&widget});
#endif
window.show();
widget.show(); widget.show();
return QApplication::exec(); return QApplication::exec();

View File

@ -481,7 +481,7 @@ UINT GetDotsPerInchForWindow(HWND handle) {
// Return hard-coded DPI if DPI scaling is disabled. // Return hard-coded DPI if DPI scaling is disabled.
return m_defaultDotsPerInch; return m_defaultDotsPerInch;
} }
if (!handle || !m_lpIsWindow(handle)) { if (!m_lpIsWindow(handle)) {
if (m_lpGetSystemDpiForProcess) { if (m_lpGetSystemDpiForProcess) {
return m_lpGetSystemDpiForProcess(m_lpGetCurrentProcess()); return m_lpGetSystemDpiForProcess(m_lpGetCurrentProcess());
} else if (m_lpGetDpiForSystem) { } else if (m_lpGetDpiForSystem) {
@ -597,23 +597,13 @@ RECT GetFrameSizeForWindow(HWND handle, bool includingTitleBar = false) {
void UpdateFrameMarginsForWindow(HWND handle) { void UpdateFrameMarginsForWindow(HWND handle) {
if (handle && m_lpIsWindow(handle)) { if (handle && m_lpIsWindow(handle)) {
// We removed the whole top part of the frame (see handling of MARGINS margins = {0, 0, 0, 0};
// WM_NCCALCSIZE) so the top border is missing now. We add it back here. if (IsDwmCompositionEnabled()) {
// Note #1: You might wonder why we don't remove just the title bar const DWMNCRENDERINGPOLICY ncrp = DWMNCRP_ENABLED;
// instead of removing the whole top part of the frame and then adding m_lpDwmSetWindowAttribute(handle, DWMWA_NCRENDERING_POLICY, &ncrp,
// the little top border back. I tried to do this but it didn't work: sizeof(ncrp));
// DWM drew the whole title bar anyways on top of the window. It seems margins = {-1, -1, -1, -1};
// that DWM only wants to draw either nothing or the whole top part of }
// the frame.
// Note #2: For some reason if you try to set the top margin to just
// the top border height (what we want to do), then there is a
// transparency bug when the window is inactive, so I've decided to add
// the whole top part of the frame instead and then we will hide
// everything that we don't need (that is, the whole thing but the
// little 1 pixel wide border at the top) in the WM_PAINT handler.
// This eliminates the transparency bug and it's what a lot of Win32
// apps that customize the title bar do so it should work fine.
const MARGINS margins = {0, 0, GetFrameSizeForWindow(handle).top, 0};
m_lpDwmExtendFrameIntoClientArea(handle, &margins); m_lpDwmExtendFrameIntoClientArea(handle, &margins);
} }
} }
@ -758,17 +748,27 @@ bool WinNativeEventFilter::nativeEventFilter(const QByteArray &eventType,
if (!data->initialized) { if (!data->initialized) {
// Avoid initializing a same window twice. // Avoid initializing a same window twice.
data->initialized = TRUE; data->initialized = TRUE;
// Restore default window style.
m_lpSetWindowLongPtrW(msg->hwnd, GWL_STYLE,
WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN |
WS_CLIPSIBLINGS);
// The following two functions can help us get rid of the three
// system buttons (minimize, maximize and close). But they also
// break the Arcylic effect (introduced in Win10 1709), don't know
// why.
m_lpSetWindowLongPtrW(msg->hwnd, GWL_EXSTYLE,
WS_EX_APPWINDOW | WS_EX_LAYERED);
m_lpSetLayeredWindowAttributes(msg->hwnd, RGB(255, 0, 255), 0,
LWA_COLORKEY);
// Trigger a frame change event to let us enter the WM_NCCALCSIZE // Trigger a frame change event to let us enter the WM_NCCALCSIZE
// message to remove our title bar as early as possible. // message to remove our title bar as early as possible.
updateWindow(msg->hwnd, true, false); updateWindow(msg->hwnd, true, false);
} // Make sure our window have it's frame shadow.
LRESULT dwmHitResult = 0; // The frame shadow is drawn by Desktop Window Manager (DWM), don't
if (!data->windowData.mouseTransparent && IsDwmCompositionEnabled() && // draw it yourself. The frame shadow will get lost if DWM
m_lpDwmDefWindowProc && // composition is disabled, it's designed to be, don't force the
m_lpDwmDefWindowProc(msg->hwnd, msg->message, msg->wParam, // window to draw a frame shadow in that case.
msg->lParam, &dwmHitResult)) { UpdateFrameMarginsForWindow(msg->hwnd);
*result = dwmHitResult;
return true;
} }
switch (msg->message) { switch (msg->message) {
case WM_NCCALCSIZE: { case WM_NCCALCSIZE: {
@ -817,6 +817,40 @@ bool WinNativeEventFilter::nativeEventFilter(const QByteArray &eventType,
// structure. On entry, the structure contains the proposed window // structure. On entry, the structure contains the proposed window
// rectangle for the window. On exit, the structure should contain // rectangle for the window. On exit, the structure should contain
// the screen coordinates of the corresponding window client area. // the screen coordinates of the corresponding window client area.
const auto getClientAreaInsets = [](HWND _hWnd) -> RECT {
// We don't need this correction when we're fullscreen. We will
// have the WS_POPUP size, so we don't have to worry about
// borders, and the default frame will be fine.
if (IsMaximized(_hWnd) && !IsFullScreen(_hWnd)) {
// Windows automatically adds a standard width border to all
// sides when a window is maximized.
int frameThickness_x =
getSystemMetric(_hWnd, SystemMetric::BorderWidth);
int frameThickness_y =
getSystemMetric(_hWnd, SystemMetric::BorderHeight);
// The following two lines are two seperate functions in
// Chromium, it uses them to judge whether the window
// should draw it's own frame or not. But here we will
// always draw our own frame because our window is totally
// frameless, so we can simply use constants here. I don't
// remove them completely because I don't want to forget
// what it's about to achieve.
const bool removeStandardFrame = true;
const bool hasFrame = !removeStandardFrame;
if (!hasFrame) {
frameThickness_x -= 1;
frameThickness_y -= 1;
}
RECT rect;
rect.top = frameThickness_y;
rect.bottom = frameThickness_y;
rect.left = frameThickness_x;
rect.right = frameThickness_x;
return rect;
}
return {0, 0, 0, 0};
};
const RECT insets = getClientAreaInsets(msg->hwnd);
const auto mode = static_cast<BOOL>(msg->wParam); const auto mode = static_cast<BOOL>(msg->wParam);
// If the window bounds change, we're going to relayout and repaint // If the window bounds change, we're going to relayout and repaint
// anyway. Returning WVR_REDRAW avoids an extra paint before that of // anyway. Returning WVR_REDRAW avoids an extra paint before that of
@ -827,32 +861,10 @@ bool WinNativeEventFilter::nativeEventFilter(const QByteArray &eventType,
const auto clientRect = mode const auto clientRect = mode
? &(reinterpret_cast<LPNCCALCSIZE_PARAMS>(msg->lParam)->rgrc[0]) ? &(reinterpret_cast<LPNCCALCSIZE_PARAMS>(msg->lParam)->rgrc[0])
: reinterpret_cast<LPRECT>(msg->lParam); : reinterpret_cast<LPRECT>(msg->lParam);
// Store the original top before the default window proc applies the clientRect->top += insets.top;
// default frame. clientRect->bottom -= insets.bottom;
const LONG originalTop = clientRect->top; clientRect->left += insets.left;
// Apply the default frame clientRect->right -= insets.right;
const LRESULT ret = m_lpDefWindowProcW(msg->hwnd, WM_NCCALCSIZE,
msg->wParam, msg->lParam);
if (ret != 0) {
*result = ret;
return true;
}
// Re-apply the original top from before the size of the default
// frame was applied.
clientRect->top = originalTop;
// We don't need this correction when we're fullscreen. We will have
// the WS_POPUP size, so we don't have to worry about borders, and
// the default frame will be fine.
if (IsMaximized(msg->hwnd) && !IsFullScreen(msg->hwnd)) {
// When a window is maximized, its size is actually a little bit
// more than the monitor's work area. The window is positioned
// and sized in such a way that the resize handles are outside
// of the monitor and then the window is clipped to the monitor
// so that the resize handle do not appear because you don't
// need them (because you can't resize a window when it's
// maximized unless you restore it).
clientRect->top += GetFrameSizeForWindow(msg->hwnd).top;
}
// Attempt to detect if there's an autohide taskbar, and if // Attempt to detect if there's an autohide taskbar, and if
// there is, reduce our size a bit on the side with the taskbar, // there is, reduce our size a bit on the side with the taskbar,
// so the user can still mouse-over the taskbar to reveal it. // so the user can still mouse-over the taskbar to reveal it.
@ -958,6 +970,19 @@ bool WinNativeEventFilter::nativeEventFilter(const QByteArray &eventType,
*result = 0; *result = 0;
return true; return true;
} }
case WM_NCPAINT: {
// 边框阴影处于非客户区的范围,因此如果直接阻止非客户区的绘制,会导致边框阴影丢失
if (IsDwmCompositionEnabled()) {
break;
} else {
// Only block WM_NCPAINT when DWM composition is disabled. If
// it's blocked when DWM composition is enabled, the frame
// shadow won't be drawn.
*result = 0;
return true;
}
}
case WM_NCACTIVATE: { case WM_NCACTIVATE: {
// DefWindowProc won't repaint the window border if lParam (normally // DefWindowProc won't repaint the window border if lParam (normally
// a HRGN) is -1. // a HRGN) is -1.
@ -968,46 +993,183 @@ bool WinNativeEventFilter::nativeEventFilter(const QByteArray &eventType,
return true; return true;
} }
case WM_NCHITTEST: { case WM_NCHITTEST: {
if (dwmHitResult != 0) { // 原生Win32窗口只有顶边是在窗口内部resize的其余三边都是在窗口
break; // 外部进行resize的其原理是WS_THICKFRAME会在窗口的左、右和
} // 底边添加三个透明的resize区域这三个区域在正常状态下是完全不可
// 见的它们由DWM负责绘制消息也由DWM负责侦测和触发。这些区域的
// 宽度等于(SM_CXSIZEFRAME + SM_CXPADDEDBORDER),高度等于
// (SM_CYSIZEFRAME + SM_CXPADDEDBORDER)在100%缩放时,均等
// 于8像素。它们属于窗口区域的一部分但不属于客户区而是属于非客
// 户区因此GetWindowRect获取的区域中是包含这三个resize区域的
// 而GetClientRect获取的区域是不包含它们的。当把
// DWMWA_EXTENDED_FRAME_BOUNDS作为参数调用
// DwmGetWindowAttribute时也能获取到一个窗口大小这个大小介
// 于前面两者之间,暂时不知道这个数据的意义及其作用。我们在
// WM_NCCALCSIZE消息的处理中已经把整个窗口都设置为客户区了
// 就是说我们的窗口已经没有非客户区了因此那三个透明的resize区
// 域,此刻也已经成为窗口客户区的一部分了,从而变得不透明了。所以
// 现在的resize看起来像是在窗口内部resize是因为原本透明的地方
// 现在变得不透明了实际上单纯从范围上来看现在我们resize的地方
// 就是普通窗口的边框外部,那三个透明区域的范围。
// 因此如果我们把边框完全去掉就是我们正在做的事情resize就
// 会看起来是在内部进行,这个问题通过常规方法非常难以解决。我测试过
// QQ和钉钉的窗口它们的窗口就是在外部resize但实际上它们是通过
// 把窗口实际的内容,嵌入到一个完全透明的但尺寸要大一圈的窗口中实现
// 的,虽然看起来效果还行,但在我看来不是正途。而且我之所以能发现,
// 也是由于这种方法在很多情况下会露馅,比如窗口未响应卡住或贴边的时
// 候,能明显看到窗口周围多出来一圈边界。我曾经尝试再把那三个区域弄
// 透明但无一例外都会破坏DWM绘制的边框阴影因此只好作罢。
if (data->windowData.mouseTransparent) { if (data->windowData.mouseTransparent) {
// Mouse events will be passed to the parent window.
*result = HTTRANSPARENT; *result = HTTRANSPARENT;
return true; return true;
} }
// This will handle the left, right and bottom parts of the frame const auto getHTResult = [](HWND _hWnd, LPARAM _lParam,
// because we didn't change them. const WINDOW *_data) -> LRESULT {
const LRESULT originalRet = m_lpDefWindowProcW( const auto isInSpecificAreas = [](int x, int y,
msg->hwnd, WM_NCHITTEST, msg->wParam, msg->lParam); const QVector<QRect> &areas,
if (originalRet != HTCLIENT) { qreal dpr) -> bool {
*result = originalRet; for (auto &&area : std::as_const(areas)) {
if (!area.isValid()) {
continue;
}
if (QRect(std::round(area.x() * dpr),
std::round(area.y() * dpr),
std::round(area.width() * dpr),
std::round(area.height() * dpr))
.contains(x, y, true)) {
return true; return true;
} }
// At this point, we know that the cursor is inside the client area }
// so it has to be either the little border at the top of our custom return false;
// title bar or the drag bar. Apparently, it must be the drag bar or };
// the little border at the top which the user can use to move or RECT clientRect = {0, 0, 0, 0};
// resize the window. m_lpGetClientRect(_hWnd, &clientRect);
RECT rcWindow = {0, 0, 0, 0}; const LONG ww = clientRect.right;
// Only GetWindowRect can give us the most accurate size of our const LONG wh = clientRect.bottom;
// window which includes the invisible resize area. POINT mouse;
m_lpGetWindowRect(msg->hwnd, &rcWindow); // Don't use HIWORD(lParam) and LOWORD(lParam) to get cursor
// Don't use HIWORD or LOWORD because they can only get positive // coordinates because their results are unsigned numbers,
// results, however, the cursor coordinates can be negative due to // however the cursor position may be negative due to in a
// in a different monitor. // different monitor.
const LONG my = GET_Y_LPARAM(msg->lParam); mouse.x = GET_X_LPARAM(_lParam);
// The top of the drag bar is used to resize the window mouse.y = GET_Y_LPARAM(_lParam);
if (!IsMaximized(msg->hwnd) && m_lpScreenToClient(_hWnd, &mouse);
(my < (rcWindow.top + GetFrameSizeForWindow(msg->hwnd).top))) { // These values are DPI-aware.
*result = HTTOP; const LONG bw =
getSystemMetric(_hWnd, SystemMetric::BorderWidth);
const LONG bh =
getSystemMetric(_hWnd, SystemMetric::BorderHeight);
const LONG tbh =
getSystemMetric(_hWnd, SystemMetric::TitleBarHeight);
const qreal dpr = GetDevicePixelRatioForWindow(_hWnd);
const bool isTitlebar = (mouse.y < tbh) &&
!isInSpecificAreas(mouse.x, mouse.y,
_data->windowData.ignoreAreas, dpr) &&
(_data->windowData.draggableAreas.isEmpty()
? true
: isInSpecificAreas(mouse.x, mouse.y,
_data->windowData.draggableAreas,
dpr));
if (IsMaximized(_hWnd)) {
if (isTitlebar) {
return HTCAPTION;
}
return HTCLIENT;
}
const bool isTop = mouse.y < bh;
const bool isBottom = mouse.y > (wh - bh);
// Make the border wider to let the user easy to resize on
// corners.
const int factor = (isTop || isBottom) ? 2 : 1;
const bool isLeft = mouse.x < (bw * factor);
const bool isRight = mouse.x > (ww - (bw * factor));
const bool fixedSize = _data->windowData.fixedSize;
const auto getBorderValue = [fixedSize](int value) -> int {
// HTBORDER: non-resizeable window border.
return fixedSize ? HTBORDER : value;
};
if (isTop) {
if (isLeft) {
return getBorderValue(HTTOPLEFT);
}
if (isRight) {
return getBorderValue(HTTOPRIGHT);
}
return getBorderValue(HTTOP);
}
if (isBottom) {
if (isLeft) {
return getBorderValue(HTBOTTOMLEFT);
}
if (isRight) {
return getBorderValue(HTBOTTOMRIGHT);
}
return getBorderValue(HTBOTTOM);
}
if (isLeft) {
return getBorderValue(HTLEFT);
}
if (isRight) {
return getBorderValue(HTRIGHT);
}
if (isTitlebar) {
return HTCAPTION;
}
return HTCLIENT;
};
*result = getHTResult(msg->hwnd, msg->lParam, data);
return true; return true;
} }
if (my < case WM_GETMINMAXINFO: {
(rcWindow.top + GetFrameSizeForWindow(msg->hwnd, true).top)) { // Don't cover the taskbar when maximized.
*result = HTCAPTION; const HMONITOR monitor =
return true; m_lpMonitorFromWindow(msg->hwnd, MONITOR_DEFAULTTONEAREST);
MONITORINFO monitorInfo;
SecureZeroMemory(&monitorInfo, sizeof(monitorInfo));
monitorInfo.cbSize = sizeof(monitorInfo);
m_lpGetMonitorInfoW(monitor, &monitorInfo);
const RECT rcWorkArea = monitorInfo.rcWork;
const RECT rcMonitorArea = monitorInfo.rcMonitor;
auto &mmi = *reinterpret_cast<LPMINMAXINFO>(msg->lParam);
if (QOperatingSystemVersion::current() <
QOperatingSystemVersion::Windows8) {
// FIXME: Buggy on Windows 7:
// The origin of coordinates is the top left edge of the
// monitor's work area. Why? It should be the top left edge of
// the monitor's area.
mmi.ptMaxPosition.x = rcMonitorArea.left;
mmi.ptMaxPosition.y = rcMonitorArea.top;
} else {
// Works fine on Windows 8/8.1/10
mmi.ptMaxPosition.x =
std::abs(rcWorkArea.left - rcMonitorArea.left);
mmi.ptMaxPosition.y =
std::abs(rcWorkArea.top - rcMonitorArea.top);
} }
*result = HTCLIENT; if (data->windowData.maximumSize.isEmpty()) {
mmi.ptMaxSize.x = std::abs(rcWorkArea.right - rcWorkArea.left);
mmi.ptMaxSize.y = std::abs(rcWorkArea.bottom - rcWorkArea.top);
} else {
mmi.ptMaxSize.x =
std::round(GetDevicePixelRatioForWindow(msg->hwnd) *
data->windowData.maximumSize.width());
mmi.ptMaxSize.y =
std::round(GetDevicePixelRatioForWindow(msg->hwnd) *
data->windowData.maximumSize.height());
}
mmi.ptMaxTrackSize.x = mmi.ptMaxSize.x;
mmi.ptMaxTrackSize.y = mmi.ptMaxSize.y;
if (!data->windowData.minimumSize.isEmpty()) {
mmi.ptMinTrackSize.x =
std::round(GetDevicePixelRatioForWindow(msg->hwnd) *
data->windowData.minimumSize.width());
mmi.ptMinTrackSize.y =
std::round(GetDevicePixelRatioForWindow(msg->hwnd) *
data->windowData.minimumSize.height());
}
*result = 0;
return true; return true;
} }
case WM_SETICON: case WM_SETICON:
@ -1026,64 +1188,11 @@ bool WinNativeEventFilter::nativeEventFilter(const QByteArray &eventType,
*result = ret; *result = ret;
return true; return true;
} }
case WM_ACTIVATE: case WM_DWMCOMPOSITIONCHANGED:
case WM_DWMCOMPOSITIONCHANGED: {
UpdateFrameMarginsForWindow(msg->hwnd); UpdateFrameMarginsForWindow(msg->hwnd);
break; break;
} default:
case WM_NCPAINT: {
if (IsDwmCompositionEnabled()) {
break; break;
} else {
// Only block WM_NCPAINT when DWM composition is disabled. If
// it's blocked when DWM composition is enabled, the frame
// shadow won't be drawn.
*result = 0;
return true;
}
}
#if 0
case WM_PAINT: {
PAINTSTRUCT ps;
const HDC hdc = m_lpBeginPaint(msg->hwnd, &ps);
const LONG topBorderHeight = 1;
if (ps.rcPaint.top < topBorderHeight) {
RECT rcTopBorder = ps.rcPaint;
rcTopBorder.bottom = topBorderHeight;
// To show the original top border, we have to paint on top
// of it with the alpha component set to 0. This page
// recommends to paint the area in black using the stock
// BLACK_BRUSH to do this:
// https://docs.microsoft.com/en-us/windows/win32/dwm/customframe#extending-the-client-frame
m_lpFillRect(hdc, &rcTopBorder, GetStockBrush(BLACK_BRUSH));
}
if (ps.rcPaint.bottom > topBorderHeight) {
RECT rcRest = ps.rcPaint;
rcRest.top = topBorderHeight;
// To hide the original title bar, we have to paint on top
// of it with the alpha component set to 255. This is a hack
// to do it with GDI. See UpdateFrameMarginsForWindow for
// more information.
HDC opaqueDc;
BP_PAINTPARAMS params;
SecureZeroMemory(&params, sizeof(params));
params.cbSize = sizeof(params);
params.dwFlags = BPPF_NOCLIP | BPPF_ERASE;
const HPAINTBUFFER buf = m_lpBeginBufferedPaint(
hdc, &rcRest, BPBF_TOPDOWNDIB, &params, &opaqueDc);
m_lpFillRect(opaqueDc, &rcRest,
reinterpret_cast<HBRUSH>(m_lpGetClassLongPtrW(
msg->hwnd, GCLP_HBRBACKGROUND)));
m_lpBufferedPaintSetAlpha(buf, nullptr, 255);
m_lpEndBufferedPaint(buf, TRUE);
}
m_lpEndPaint(msg->hwnd, &ps);
break;
}
#endif
default: {
break;
}
} }
} }
return false; return false;

View File

@ -41,6 +41,7 @@ public:
BOOL fixedSize = FALSE, mouseTransparent = FALSE; BOOL fixedSize = FALSE, mouseTransparent = FALSE;
int borderWidth = -1, borderHeight = -1, titlebarHeight = -1; int borderWidth = -1, borderHeight = -1, titlebarHeight = -1;
QVector<QRect> ignoreAreas, draggableAreas; QVector<QRect> ignoreAreas, draggableAreas;
QSize maximumSize = {-1, -1}, minimumSize = {-1, -1};
}; };
using WINDOW = struct _WINDOW { using WINDOW = struct _WINDOW {