Linux Qt 无边框窗体的移动以及禁止使用窗体的最大化

2018年6月25日 12.57k 次阅读 0 条评论 6 人点赞

Linux Qt 无边框窗体移动的问题

网上有多种移动无边框 Qt 程序的方法,集中在通过 MousePressEvent 以及 MouseMoveEvent 两个事件的处理上,不过在 Linux 平台上,他们实现的移动效果不能满足我的要求,因为在 Gnome 的环境中无法移动出屏幕左侧(Ubuntu 系统中),这一点很不友好;在 QML 程序中,通过对 MouseArea 中重载 onClicked 函数以及 onPositionChanged 函数实现窗体移动,这两个函数的原理也是与 MousePressEvent 类似,实现的效果也类似,而且由于 QML 的绘制是由 OpenGl 进行的绘制,在 Qt5 中,这一部分还存在 BUG,移动时,窗体会出现闪烁的问题。

为了解决这个问题,通过对【网易云音乐 Linux 版】这个软件的分析,发现他调用的是 XLIB 库进行的窗体移动,所以为了实现类似的效果,通过 Google 了解到了如何通过 Xlib 库进行窗体移动,不过一般都没有实际的 Example,只查到可以通过 XSendEvent 设置 _NET_WM_MOVERESIZE 选项实现,正当我不知道如何使用的时候,无意间跳转到 github 网站,然后在 github 站内搜索,这里就会有许多的参考用例,在 Linux 平台许多的窗体程序就是调用 XLIB 进行的窗体移动,通过一番调试,最后成型的代码如下:

void System::MoveWindow(void)
{
        XEvent event;
        memset(&event, 0, sizeof(XEvent));
        const auto pos = QCursor::pos();

        Display *display = QX11Info::display();
        event.xclient.type = ClientMessage;
        event.xclient.message_type =
            XInternAtom(display, "_NET_WM_MOVERESIZE", False);
        event.xclient.display = display;
        //wid 是当前程序的 window id,可以通过 QWidget->wId()获得,QWidget 必须实例化
        event.xclient.window = wid;
        event.xclient.format = 32;
        event.xclient.data.l[0] = pos.x();
        event.xclient.data.l[1] = pos.y();
        event.xclient.data.l[2] = 8;
        event.xclient.data.l[3] = Button1;
        event.xclient.data.l[4] = 1;

        XUngrabPointer(display, CurrentTime);
        XSendEvent(display, QX11Info::appRootWindow(QX11Info::appScreen()),
                   False, SubstructureNotifyMask | SubstructureRedirectMask,
                   &event);
        XFlush(display);
}

Linux Qt 无边框窗体禁止最大化与 Resize

即使我们定义了无边框的窗体,Linux 窗体管理器仍然会接管到你的窗体,当你对一个窗体按下 ALT + Space 时可以调出一个 Menu,他可以控制窗体的最大最小化以及移动等属性,这些设置都属于 Window Manager 处理的,不是你的 QT 程序能够接管的部分,假如我们并不希望我们的程序能够被最大化,那么仅仅使用 Qt 内部的隐藏最大化按钮是不能完全解决这个问题的,所以我们仍然需要用到 Xlib,为什么 Xlib 能够管理到 Window Manager 呢?因为 Xorg 程序是一个服务程序,整个系统中仅仅存在这个一个图形显示程序,其他的所有的图形程序都只是一个客户端,都需要将自己的图形处理发送给 Xorg 进行处理,也就是我能控制到 Xorg 就能控制整个系统的图形,只要我知道这个图形程序的 Wid,也是前文中提到的 WID,窗口句柄。

言归正传,既然知道控制 Xorg 就能控制整个窗体,那么 XLIB 就是控制 Xorg 的一个库,通过调用这个库就可以达到控制 Xorg 的效果,这一点上文的窗体移动已经看的出来了,为了实现大小固定, Qt 程序可以调用 setFixedSize 实现,但是我不知道为什么在 Qt 5.6 中,这个函数会将最小化按钮也禁止掉,也就是说我一旦设置了大小固定,那么最小化也不允许,我不知道这个是 QT 的 BUG,还是他们官方故意弄的一个狗屁设定,反正我需要的效果仅仅只是不允许最大化也不能 resize 窗体即可,但是最小化你的给我留着啊。好吧,既然 Qt 做不到那就只能请 Xlib 了,通过查阅 XLIB 官方手册,有专门讲到这一个功能的实现(好奇怪,还专门写了),要实现这个效果,只需要将窗体的 MaxSize 以及 MinSize 设置为一样就行了,那么问题来了,难道 Qt 中不是调用这个参数实现的?于是我又重新试了不调用 setFixedSize 而是去使用 Qt 中的 setMaxiumWidth 与 setMiniumWidth 函数进行测试,结果和 setFixedSize 一样,把我逼急了就去查看了 Qt 的源码,发现 setFixedSize 本来就是调用的这两个函数去固定长与宽的,我靠,也就是 Qt 还是做不到,难道 XLIB 的手册有错误?抱着试一试的态度我就去试试 XLIB 来设置大小,还是没有 example,老规矩,查 github.com 去找别人的代码,我去,居然成功了,还真是手册要求的那样,那么 QT 为什么会将最小化也禁止掉呢?脑子抽了?

贴代码:

XSizeHints *hints = XAllocSizeHints();
long userhints;
XGetWMNormalHints(QX11Info::display(), view.winId(), hints, &userhints);
hints->max_width = hints->min_width = 590;
hints->max_height = hints->min_height = 435;
hints->flags |= PMaxSize | PMinSize;
XSetWMNormalHints(QX11Info::display(), view.winId(), hints);
XFree(hints);
XFlush(QX11Info::display());
[2016-08-17 更新]

最近重新调整这个软件的时候发现,在低版本的 RHEL6 上进行静态 Qt 的编译发现上面这一段禁止窗体 Resize 的代码无法得到正确的效果(PS,Ubuntu 14.10 以及 RHEL7 版本可以得到效果),这就让我蛋疼了,因为软件需要能够全平台覆盖(所有的 RHEL6 以及 Ubuntu 14.04 以上的 Linux 版本),看起来这一段代码是不能够满足我的要求,那么只能继续使用 Qt 库自带的 setFixedSize 这个函数,虽然不能最小化但是为了全平台我也没有办法,当然不是真的没有办法,为了验证为什么 setFixedSize 会将最小化也一同禁止掉,特意查看了 Qt 4.8.7 的源码,代码如下:

if (q->minimumSize() == q->maximumSize()) {
        // fixed size, remove the resize handle (since mwm/dtwm
        // isn't smart enough to do it itself)
        mwmhints.flags |= MWM_HINTS_FUNCTIONS;
        if (mwmhints.functions == MWM_FUNC_ALL) {
                mwmhints.functions = MWM_FUNC_MOVE;
        } else {
                mwmhints.functions &= ~MWM_FUNC_RESIZE;
        }
        if (mwmhints.decorations == MWM_DECOR_ALL) {
                mwmhints.flags |= MWM_HINTS_DECORATIONS;
                mwmhints.decorations = (MWM_DECOR_BORDER
                | MWM_DECOR_TITLE
                | MWM_DECOR_MENU);
        } else {
                mwmhints.decorations &= ~MWM_DECOR_RESIZEH;
        }
        if (q->windowFlags() & Qt::WindowMinimizeButtonHint) {
                mwmhints.flags |= MWM_HINTS_DECORATIONS;
                mwmhints.decorations |= MWM_DECOR_MINIMIZE;
                mwmhints.functions |= MWM_FUNC_MINIMIZE;
        }
        if (q->windowFlags() & Qt::WindowMaximizeButtonHint) {
                mwmhints.flags |= MWM_HINTS_DECORATIONS;
                mwmhints.decorations |= MWM_DECOR_MAXIMIZE;
                mwmhints.functions |= MWM_FUNC_MAXIMIZE;
        }
        if (q->windowFlags() & Qt::WindowCloseButtonHint)
                mwmhints.functions |= MWM_FUNC_CLOSE;
}

源码位于 src/gui/kernel/qwidget_x11.cpp 中,可以看的出来,当 maximumSize == minimumSize 时,Qt 会对 Linux 的窗口管理器发送一些设置参数,其中就包括了 MWM_FUNC_MINIMIZE,也就是当窗体的 windowFlags 不包含 Qt::WindowMinimizeButtonHint 这个标记时,就会禁用掉最小化,看到这里我就明白了,原来当设置 setFixedSize 时,如果不显式的声明需要最小化,系统是会将最小化也一同禁止掉的,同样的,也可以在 setFixedSize 是打开最大化属性,关闭属性等等。

基于此,我只需要在 setFixedSize 函数调用之前设置窗体的最小化标记显式声明即可。

w.setWindowFlags(Qt::WindowMinimizeButtonHint | Qt::FramelessWindowHint);
w.setFixedSize(WINDOW_WIDTH, WINDOW_HEIGHT);
w.show();

这里还是需要加上Qt::FramelessWindowHint不然你的程序会有窗口管理器提供的一个标题栏,最后在各个平台运行实验,一切正常,窗体不但能固定大小,也能正确的最小化。(PS,在 RHEL6 上会有一个窗口管理器提供的标题栏,虽然已经设置了 Qt::FramelessWindowHint 属性,但是毕竟怎么实现你设置的标志是 Linux 窗口管理器自己在做的,因此在低版本的系统上,表现不一致也是可以理解的,不过虽然会有这一点小瑕疵,但是并不影响使用,也算是大功告成)

最后,上面自己写的那段代码提到的 Qt 无法实现我想要的效果这个结论是错误的,不过我还是保留了下来,毕竟,犯错误也是一个学习的过程。

标签:
最后编辑:2020年12月30日