user-interfacetaskbarpyqt6docki3

Set a QApplication on top of the screen with reserved space (like a taskbar)


General overview

I try to set just a minimal taskbar (like Polybar or i3bar, or any task bar you know).

The constraint are the folowing ones:

  1. It should have a dedicate space to avoid overlaping with other windows;
  2. It should appear always on top;
  3. The two above constraints should also work on i3wm.

The Problem I face

For the moment, I could make it always on top of the screen (constraint 1), but it jut like floating window here, other windows don’t see it space as dedicate (constraint 2), as you can see in the following screenshot:

Rendering of the current code

Minimal Working Example

And this is the code who produced the above rendering:

import sys
from PyQt6.QtWidgets import QApplication, QWidget, QHBoxLayout, QLabel
from PyQt6.QtCore import Qt, QRect
from PyQt6.QtGui import QGuiApplication

from Xlib import display, Xatom, X

class StatusBar(QWidget):
    def __init__(self):
        super().__init__()

        # 1. Set height and get screen width
        self.setFixedHeight(58)
        screen_geometry = QGuiApplication.primaryScreen().geometry()
        self.setGeometry(QRect(0, 0, screen_geometry.width(), 58))

        # 2. Style
        self.setStyleSheet("background-color: black; color: white;")

        # 3. Layout
        layout = QHBoxLayout()
        layout.addWidget(QLabel("Test"))
        layout.setContentsMargins(10, 0, 10, 0)
        self.setLayout(layout)

        # 4. Window flags (no decoration, always on top)
        self.setWindowFlags(
            Qt.WindowType.FramelessWindowHint |
            Qt.WindowType.Tool |
            Qt.WindowType.X11BypassWindowManagerHint  # Important for override redirect
        )

#############################################################################
# The accurate part                                                         #
#############################################################################
    def reserve_space(self):                                                #
        d = display.Display()                                               #
        wid = int(self.winId())                                             #
        win = d.create_resource_object('window', wid)                       #
                                                                            #
        screen_width = QGuiApplication.primaryScreen().geometry().width()   #
        bar_height = self.height()                                          #
                                                                            #
        # Set window type to DOCK                                           #
        win.change_property(                                                #
            d.intern_atom('_NET_WM_WINDOW_TYPE'),                           #
            Xatom.ATOM, 32,                                                 #
            [d.intern_atom('_NET_WM_WINDOW_TYPE_DOCK')]                     #
        )                                                                   #
                                                                            #
        # Set struts                                                        #
        win.change_property(                                                #
            d.intern_atom('_NET_WM_STRUT'),                                 #
            Xatom.CARDINAL, 32,                                             #
            [0, 0, bar_height, 0]  # left, right, top, bottom               #
        )                                                                   #
                                                                            #
        win.change_property(                                                #
            d.intern_atom('_NET_WM_STRUT_PARTIAL'),                         #
            Xatom.CARDINAL, 32,                                             #
            [                                                               #
                0, 0, bar_height, 0,         # left, right, top, bottom     #
                0, 0,                        # left_start_y, left_end_y     #
                0, 0,                        # right_start_y, right_end_y   #
                0, screen_width - 1,         # top_start_x, top_end_x       #
                0, 0                         # bottom_start_x, bottom_end_x #
            ]                                                               #
        )                                                                   #
                                                                            #
        d.flush()                                                           #
#############################################################################

if __name__ == "__main__":
    app = QApplication(sys.argv)
    bar = StatusBar()
    bar.show()
    bar.reserve_space()
    sys.exit(app.exec())

Some observation

Lot of applications witch have this behavior exist, like Polybar or i3bar as mentioned before. They also work very fine, if I run two of this kind of taskbar, they don’t overlap, they respect each other position and the last one come under the previous one. So it seems to be a stand.

The question

How to get a behavior similar to the other taskbars?


Solution

  • After many tries, I find the following working examples.

    import sys
    from PyQt6.QtWidgets import QApplication, QLabel, QWidget, QHBoxLayout
    from PyQt6.QtCore import Qt
    from PyQt6.QtGui import QGuiApplication
    from Xlib import display, X, Xatom
    
    class TopBar(QWidget):
        def __init__(self):
            super().__init__()
    
            # Screen Size
            screen_geometry = QGuiApplication.primaryScreen().geometry()
            screen_width = screen_geometry.width()
    
            self.setFixedSize(screen_width, 50)
            self.move(0, 0)
    
            # Some contents
            self.setStyleSheet("background-color: #222; color: white; font-size: 20px;")
            layout = QHBoxLayout()
            label = QLabel("Test")
            label.setAlignment(Qt.AlignmentFlag.AlignCenter)
            layout.addWidget(label)
            self.setLayout(layout)
    
            # Panelization
            self.setWindowFlags(
                Qt.WindowType.FramelessWindowHint |
                Qt.WindowType.WindowStaysOnTopHint | # <- One of the most important lines
                Qt.WindowType.Tool
            )
    
            # Set 
            self.reserve_space()
    
        def reserve_space(self):
            d = display.Display()
            root = d.screen().root
            window_id = self.winId().__int__()
    
            NET_WM_STRUT_PARTIAL = d.intern_atom('_NET_WM_STRUT_PARTIAL')
            NET_WM_STRUT = d.intern_atom('_NET_WM_STRUT')
            NET_WM_WINDOW_TYPE = d.intern_atom('_NET_WM_WINDOW_TYPE')
            NET_WM_WINDOW_TYPE_DOCK = d.intern_atom('_NET_WM_WINDOW_TYPE_DOCK')
    
            # Retrive the size to reserve it
            width = self.width()
            height = self.height()
    
            top = height  # reserv the height of the bar
    
            strut = [0, 0, top, 0]
            strut_partial = [0, 0, top, 0, 0, width, 0, 0, 0, 0, 0, 0]
    
            root.change_property(NET_WM_STRUT, Xatom.CARDINAL, 32, strut)
            root.change_property(NET_WM_STRUT_PARTIAL, Xatom.CARDINAL, 32, strut_partial)
    
            window = d.create_resource_object('window', window_id)
            window.change_property(NET_WM_WINDOW_TYPE, Xatom.ATOM, 32, [NET_WM_WINDOW_TYPE_DOCK])
    
            d.sync()
    
    if __name__ == "__main__":
        app = QApplication(sys.argv)
        bar = TopBar()
        bar.show()
        sys.exit(app.exec())
    
    

    I tried quickly to compare it with the original one to see what is the main change, but I didn’t really find. It seems that Qt.WindowType.WindowStaysOnTopHint changed many things.