本文中所说的窗口都是指 decorated top-level widget,对于简单程序来说通常就是 QMainWindow 的子类对象。

之前玩 Qt 的时候一直以为 QMainWindow 的大小就是 .geometry() 报告的大小(也就是 .width().height()),结果今天发现其实不对的。

起因是我想将一个窗口放在整个桌面的右下角。那么这里有几个问题。

首先 Windows 平台默认下面是任务栏,那么我们肯定不希望窗口的内容被任务栏挡住(或者挡住任务栏),所以这里需要使用 QDesktopWidget.availableGeometry() 来获取可用的屏幕。在 Windows 平台上,这个方法计算除了任务栏之外的屏幕区域;在 Mac 上会计算除了 Dock 栏和菜单栏的屏幕区域。(什么?Linux?抱歉我已经不在桌面上使用 Linux)

那么可能我们就会写出这样的代码

class MyWindow(QtGui.QMainWindow):

    def __init__(self):
        QtGui.QMainWindow.__init__(self)
        desktop_geopmetry = QtGui.qApp.desktop().availableGeometry()
        self.move(
            desktop_geometry.width() - self.width(),
            desktop_geometry.height() - self.height()
        )

那么显然我们就是 too young。因为根据 Qt 文档.geometry() 这一类的方法返回窗口 widget 区域(相对应地,.width().height() 都是返回窗口 widget 的宽高),而真实的窗口还带有操作系统窗口管理器附加的装饰,例如标题栏和额外的边框。看下图就懂了(图片来自 Qt 文档,链接在本段头部)。

geometry.png

好吧,那么我们现在改成这样就可以了吧?

class MyWindow(QtGui.QMainWindow):

    def __init__(self):
        QtGui.QMainWindow.__init__(self)
        desktop_geopmetry = QtGui.qApp.desktop().availableGeometry()
        self_geometry = self.frameGeometry()
        self.move(
            desktop_geometry.width() - self_geometry.width(),
            desktop_geometry.height() - self_geometry.height()
        )

那么显然我们又是 too simple。因为我测试了发现,如果这个窗口还没显示(__init__ 里显然还没显示),那么 .frameGeometry() 这类方法的返回结果其实跟 .geometry() 这一类是一样的。换句话说,只有窗口被至少显示过一次,窗口管理器才有机会去装饰它,这样 Qt 才能知道最终窗口的实际占用区域大小。

那么我们只要在首次 showEvent 信号被触发的时候来获取窗口大小,然后据此移动窗口就可以了(当然如果你想每次显示都移动一下我也没话说 =,=)。

class MyWindow(QtGui.QMainWindow):

    def __init__(self):
        QtGui.QMainWindow.__init__(self)
        self.__first_show = True

    def showEvent(self, evt):
        evt.accept()
        if self.__first_show:
            desktop_geopmetry = QtGui.qApp.desktop().availableGeometry()
            self_geometry = self.frameGeometry()
            self.move(
                desktop_geometry.width() - self_geometry.width(),
                desktop_geometry.height() - self_geometry.height()
            )
            self.__first_show = False