最近写一个小工具,选用QT作为界面库,一方面是QT功能强大,另外也是因为自己对QT比较熟悉,下面记录一下对无边框窗口改变大小的一些实现。
首先定义一个控件类,继承自QWidget,当然继承自QFrame也可以,根据自己需要而定了,然后设置隐藏边框 setWindowFlags(Qt::FramelessWindowHint);。第一步就完成了,这样就出现一个没有边框没有标题栏的窗口,运行。。。问题就出来了,窗口移动不了,改变不了大小,也没有按钮可以让我们关闭。下面我们逐条来解决这些问题。
1 窗口移动:这个可以很快想到,在鼠标按下并移动的时候对窗口移动,OK,这个可以解决了。
2 拖动改变大小:由第一条可以同理推出,在鼠标离窗口边缘一定距离的时候,就认为是可以改变窗口大小状态,这时候我们改变鼠标指针,当鼠标按下时,就认为开始改变窗口大小,移动鼠标的时候就重新计算窗口位置,OK,这个也搞定了。
运行上面结果发现,移动窗口没问题,但改变大小就不爽了,很卡,不流畅,优化各项计算,还是不流畅。google了一下,发现有个实现好了的NcFramelessHelper可以使用,于是就下载看了一下,他和上面实现基本相同,拿这个运行了一下发现效果跟我自己的一样,也是有点卡,不流畅。
看来用这种方式存在一定问题,需要寻找其他路径。重新搜索了下,发现在 http://www.slashbackassociates.com/blog/Borderless-window-resizing-with-WM-NCHITTEST里面讲到用WM_NCHITTEST,该消息在MSDN上面解释如下:
Sent to a window in order to determine what part of the window corresponds to a particular screen coordinate. This can happen, for example, when the cursor moves, when a mouse button is pressed or released, or in response to a call to a function such as WindowFromPoint.
它的消息响应函数会根据鼠标当前坐标来频段鼠标命中了窗口的哪个部位,函数返回值指出部位,如HTTOP, HTTOPLEFT, HTCAPTION等。看到这里就会明白,重新定义WM_NCHITTEST,让他返回当前位置为标题栏或边框的话,就可以达到我们想要的结果了。比如移动只需要将鼠标当前位置指定为HTCAPTION即可,这样看来实现更简单。
以下是实现代码:
bool MyFrameWindow::
winEvent( MSG *message, long *result )
{
switch(message->message)
{
case WM_NCLBUTTONDBLCLK:
{
HWND hWnd = this->winId();
if(::IsZoomed(hWnd))
{
ShowWindow(hWnd, SW_RESTORE);
}
else
{
ShowWindow(hWnd, SW_MAXIMIZE);
}
return true;
}
break;
case WM_NCHITTEST:
{
int x = GET_X_LPARAM(message->lParam);
int y = GET_Y_LPARAM(message->lParam);
QPoint pt = mapFromGlobal(QPoint(x, y));
*result = OnTestBorder(pt);
if (*result == HTCLIENT)
{
QWidget* tempWidget = this->childAt(pt.x(), pt.y());
if (tempWidget == NULL || tempWidget->inherits("MyWindowTitle"))
{
*result = HTCAPTION;
}
}
return true;
}
break;
default:
break;
}
return false;
}
在上面的代码中首先是定义了WM_NCLBUTTONDBLCLK消息操作,将他设置为,在左键双击的时候,将窗口最大化或者还原,IsZoomed用来判断窗口是否处于最大化状态,如果事最大化,使用ShowWindow(hWnd, SW_RESTORE);将窗口还原,否则,ShowWindow(hWnd, SW_MAXIMIZE);使窗口最大化。
WM_NCHITTEST消息就是我们要重点关注的了,首先,用GET_X_LPARAM,GET_Y_LPARAM计算出鼠标在窗口中的位置,在用mapFromGlobal将该位置转化为屏幕坐标(不转化为屏幕坐标也是可以的,只是后续计算稍有不同而已),然后调用OnTestBorder(该函数实现在本文最后给出)计算是否在窗口边缘处,如果在边缘处,返回他的位置,如HTTOP ,HTLEFT等,这样,WINDOWS会认为已经在窗口边框上面,会按照正常窗口来操作,这样我们的鼠标形状及改变大小都交给WINDOWS完成了。*result == HTCLIENT指出不是在窗口边缘,那我们需要计算出那些地方要当成标题栏来完成窗口的拖动操作,在本例中认为在没有控件的空白区都当成是标题栏,可以对窗口进行拖动,使用childAt来查找当前坐标是否有子控件存在,如果没有就返回HTCAPTION。这里需要注意,一定要判断是否有控件存在,否则消息到不了子控件,就没法对子控件操作。另外有个tempWidget->inherits("MyWindowTitle"),MyWindowTitle是我自己对窗口放了一个标题栏,如果位置落在它上面,也认为是HTCAPTION。
经过上面修改后,基本功能已经完成,重新运行,效果很好,跟有边框窗口一样,一点也不卡,大功告成。下面是OnTestBorder实现,很简单,这里就不在讲述。
LRESULT MyFrameWindow::OnTestBorder(const QPoint &pt)
{
if(::IsZoomed(this->winId()))
{
return HTCLIENT;
}
int borderSize = 4;
int cx = this->size().width();
int cy = this->size().height();
QRect rectTopLeft(0, 0, borderSize, borderSize);
if(rectTopLeft.contains(pt))
{
return HTTOPLEFT;
}
QRect rectLeft(0, borderSize, borderSize, cy - borderSize * 2);
if(rectLeft.contains(pt))
{
return HTLEFT;
}
QRect rectTopRight(cx - borderSize, 0, borderSize, borderSize);
if(rectTopRight.contains(pt))
{
return HTTOPRIGHT;
}
QRect rectRight(cx - borderSize, borderSize, borderSize, cy - borderSize * 2);
if(rectRight.contains(pt))
{
return HTRIGHT;
}
QRect rectTop(borderSize, 0, cx - borderSize * 2, borderSize);
if(rectTop.contains(pt))
{
return HTTOP;
}
QRect rectBottomLeft(0, cy - borderSize, borderSize, borderSize);
if(rectBottomLeft.contains(pt))
{
return HTBOTTOMLEFT;
}
QRect rectBottomRight(cx - borderSize, cy - borderSize, borderSize, borderSize);
if(rectBottomRight.contains(pt))
{
return HTBOTTOMRIGHT;
}
QRect rectBottom(borderSize, cy - borderSize, cx - borderSize * 2, borderSize);
if(rectBottom.contains(pt))
{
return HTBOTTOM;
}
return HTCLIENT;
}