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

View File

@ -481,7 +481,7 @@ UINT GetDotsPerInchForWindow(HWND handle) {
// Return hard-coded DPI if DPI scaling is disabled.
return m_defaultDotsPerInch;
}
if (!handle || !m_lpIsWindow(handle)) {
if (!m_lpIsWindow(handle)) {
if (m_lpGetSystemDpiForProcess) {
return m_lpGetSystemDpiForProcess(m_lpGetCurrentProcess());
} else if (m_lpGetDpiForSystem) {
@ -597,23 +597,13 @@ RECT GetFrameSizeForWindow(HWND handle, bool includingTitleBar = false) {
void UpdateFrameMarginsForWindow(HWND handle) {
if (handle && m_lpIsWindow(handle)) {
// We removed the whole top part of the frame (see handling of
// WM_NCCALCSIZE) so the top border is missing now. We add it back here.
// Note #1: You might wonder why we don't remove just the title bar
// instead of removing the whole top part of the frame and then adding
// the little top border back. I tried to do this but it didn't work:
// DWM drew the whole title bar anyways on top of the window. It seems
// 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};
MARGINS margins = {0, 0, 0, 0};
if (IsDwmCompositionEnabled()) {
const DWMNCRENDERINGPOLICY ncrp = DWMNCRP_ENABLED;
m_lpDwmSetWindowAttribute(handle, DWMWA_NCRENDERING_POLICY, &ncrp,
sizeof(ncrp));
margins = {-1, -1, -1, -1};
}
m_lpDwmExtendFrameIntoClientArea(handle, &margins);
}
}
@ -758,17 +748,27 @@ bool WinNativeEventFilter::nativeEventFilter(const QByteArray &eventType,
if (!data->initialized) {
// Avoid initializing a same window twice.
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
// message to remove our title bar as early as possible.
updateWindow(msg->hwnd, true, false);
}
LRESULT dwmHitResult = 0;
if (!data->windowData.mouseTransparent && IsDwmCompositionEnabled() &&
m_lpDwmDefWindowProc &&
m_lpDwmDefWindowProc(msg->hwnd, msg->message, msg->wParam,
msg->lParam, &dwmHitResult)) {
*result = dwmHitResult;
return true;
// Make sure our window have it's frame shadow.
// The frame shadow is drawn by Desktop Window Manager (DWM), don't
// draw it yourself. The frame shadow will get lost if DWM
// composition is disabled, it's designed to be, don't force the
// window to draw a frame shadow in that case.
UpdateFrameMarginsForWindow(msg->hwnd);
}
switch (msg->message) {
case WM_NCCALCSIZE: {
@ -817,6 +817,40 @@ bool WinNativeEventFilter::nativeEventFilter(const QByteArray &eventType,
// structure. On entry, the structure contains the proposed window
// rectangle for the window. On exit, the structure should contain
// 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);
// If the window bounds change, we're going to relayout and repaint
// 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
? &(reinterpret_cast<LPNCCALCSIZE_PARAMS>(msg->lParam)->rgrc[0])
: reinterpret_cast<LPRECT>(msg->lParam);
// Store the original top before the default window proc applies the
// default frame.
const LONG originalTop = clientRect->top;
// Apply the default frame
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;
}
clientRect->top += insets.top;
clientRect->bottom -= insets.bottom;
clientRect->left += insets.left;
clientRect->right -= insets.right;
// Attempt to detect if there's an autohide taskbar, and if
// 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.
@ -958,6 +970,19 @@ bool WinNativeEventFilter::nativeEventFilter(const QByteArray &eventType,
*result = 0;
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: {
// DefWindowProc won't repaint the window border if lParam (normally
// a HRGN) is -1.
@ -968,46 +993,183 @@ bool WinNativeEventFilter::nativeEventFilter(const QByteArray &eventType,
return true;
}
case WM_NCHITTEST: {
if (dwmHitResult != 0) {
break;
}
// 原生Win32窗口只有顶边是在窗口内部resize的其余三边都是在窗口
// 外部进行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) {
// Mouse events will be passed to the parent window.
*result = HTTRANSPARENT;
return true;
}
// This will handle the left, right and bottom parts of the frame
// because we didn't change them.
const LRESULT originalRet = m_lpDefWindowProcW(
msg->hwnd, WM_NCHITTEST, msg->wParam, msg->lParam);
if (originalRet != HTCLIENT) {
*result = originalRet;
return true;
const auto getHTResult = [](HWND _hWnd, LPARAM _lParam,
const WINDOW *_data) -> LRESULT {
const auto isInSpecificAreas = [](int x, int y,
const QVector<QRect> &areas,
qreal dpr) -> bool {
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 false;
};
RECT clientRect = {0, 0, 0, 0};
m_lpGetClientRect(_hWnd, &clientRect);
const LONG ww = clientRect.right;
const LONG wh = clientRect.bottom;
POINT mouse;
// Don't use HIWORD(lParam) and LOWORD(lParam) to get cursor
// coordinates because their results are unsigned numbers,
// however the cursor position may be negative due to in a
// different monitor.
mouse.x = GET_X_LPARAM(_lParam);
mouse.y = GET_Y_LPARAM(_lParam);
m_lpScreenToClient(_hWnd, &mouse);
// These values are DPI-aware.
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;
}
case WM_GETMINMAXINFO: {
// Don't cover the taskbar when maximized.
const HMONITOR monitor =
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);
}
// 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
// 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
// resize the window.
RECT rcWindow = {0, 0, 0, 0};
// Only GetWindowRect can give us the most accurate size of our
// window which includes the invisible resize area.
m_lpGetWindowRect(msg->hwnd, &rcWindow);
// Don't use HIWORD or LOWORD because they can only get positive
// results, however, the cursor coordinates can be negative due to
// in a different monitor.
const LONG my = GET_Y_LPARAM(msg->lParam);
// The top of the drag bar is used to resize the window
if (!IsMaximized(msg->hwnd) &&
(my < (rcWindow.top + GetFrameSizeForWindow(msg->hwnd).top))) {
*result = HTTOP;
return true;
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());
}
if (my <
(rcWindow.top + GetFrameSizeForWindow(msg->hwnd, true).top)) {
*result = HTCAPTION;
return true;
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 = HTCLIENT;
*result = 0;
return true;
}
case WM_SETICON:
@ -1026,65 +1188,12 @@ bool WinNativeEventFilter::nativeEventFilter(const QByteArray &eventType,
*result = ret;
return true;
}
case WM_ACTIVATE:
case WM_DWMCOMPOSITIONCHANGED: {
case WM_DWMCOMPOSITIONCHANGED:
UpdateFrameMarginsForWindow(msg->hwnd);
break;
}
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;
}
}
#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);
default:
break;
}
#endif
default: {
break;
}
}
}
return false;
}

View File

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