Search code examples
pythonqtpyside

How to fix python pyside application to detect all monitors while centering an object on the primary monitor


I want to create a window that spans across all monitors, in my case I have 3. The window is a semi-transparent background with a draggable square. The square is used to provide a position for a different part of my code.

When I make the window span all 3 monitors, the square is being created on my left one which is not my primary and the position is not corresponding well. When I change the code and only make focus on the primary screen, everything works but the background no longer spans all 3 monitors.

Here is the code where all 3 monitors have the background, and the square is placed INCORRECTLY. I want the effect of spanning across all 3 monitors but not the badly placed square.

class DynamicAlignmentWindow(QWidget):
    positionSelected = Signal(int, int)

    def __init__(self, main_window, width, height):
        super().__init__()
        self.main = main_window
        self.main.hide()

        self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint)
        self.setWindowOpacity(0.5)
        self.setStyleSheet("background-color: rgba(0, 0, 255, 38);")

        # Set the geometry to span all monitors
        total_screen_rect = calculate_total_geometry()
        self.setGeometry(total_screen_rect)

        # Get the geometry of the primary screen
        primary_screen_geometry = QApplication.primaryScreen().geometry()

        self.lastPosition = None  # Initialize last known position

        print(f"Total Screen Rect: {total_screen_rect}")  # Debugging output for total geometry
        print(f"Primary Screen Rect: {primary_screen_geometry}")  # Debugging output for primary screen

        # Calculate center position for the square on the primary screen
        center_x = primary_screen_geometry.x() + (primary_screen_geometry.width() - width) // 2
        center_y = primary_screen_geometry.y() + (primary_screen_geometry.height() - height) // 2
        print(f"Centering at: X={center_x}, Y={center_y}")  # Debugging output for centering

        # Initialize the square and center it on the primary screen
        self.rubber_band = QRubberBand(QRubberBand.Rectangle, self)
        self.rubber_band.setGeometry(QRect(QPoint(center_x, center_y), QSize(width, height)))
        self.rubber_band.show()

        self.dragging = False
        self.drag_position = QPoint()

I just want the 3 monitors background from here.

Visual For clarity

Here is the code where only the primary monitor has the background and the square is placed correctly. I want all 3 monitors to have the background like the above example but this square position.

class DynamicAlignmentWindow(QWidget):
    positionSelected = Signal(int, int)

    def __init__(self, main_window, width, height):
        super().__init__()
        self.main = main_window
        self.main.hide()

        self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint)
        self.setWindowOpacity(0.5)
        self.setStyleSheet("background-color: rgba(0, 0, 255, 38);")

        primary_screen_geometry = QApplication.primaryScreen().geometry()
        self.setGeometry(primary_screen_geometry)

        self.lastPosition = None  # Initialize last known position

        print(f"Primary Screen Rect: {primary_screen_geometry}")  # Debugging output

        center_x = primary_screen_geometry.x() + (primary_screen_geometry.width() - width) // 2
        center_y = primary_screen_geometry.y() + (primary_screen_geometry.height() - height) // 2
        print(f"Centering at: X={center_x}, Y={center_y}")  # More debugging output

        self.rubber_band = QRubberBand(QRubberBand.Rectangle, self)
        self.rubber_band.setGeometry(QRect(QPoint(center_x, center_y), QSize(width, height)))
        self.rubber_band.show()

        self.dragging = False
        self.drag_position = QPoint()

I just want the position of the square here.

Visual

I dont think this below function is the problem but im adding it in case it helps solving this problem:

Finding geometry for all screens

def calculate_total_geometry():
    total_rect = QRect()
    for screen in QApplication.screens():
        total_rect = total_rect.united(screen.geometry())
    return total_rect

UPDATE

When I Use the below code, I believe Im getting the correct primary screen. I am getting:

Total Screen Rect: PySide6.QtCore.QRect(-1920, 0, 5760, 1081)

However, this centers the square on my leftmost screen which is not my primary. I wouldn't mind this however, the position is not corresponding later on. My app displays an overlay using a selected image. When the square is being centered on the left screen and I change the its position, the image is being overlayed one screen down.

For example, if I place the square top left on left screen, its overlaying top left center screen. So the position does not correspond correctly. I am not sure exactly why this happens, but I think if I get the sqaure to generate on the main center screen or the users primary screen, the position will correspond correctly.  

self.lastPosition = None  # Initialize last known position

        self.setGeometry(calculate_total_geometry())
        total_screen_rect = calculate_total_geometry()
        print(f"Total Screen Rect: {total_screen_rect}")  # Debugging output

        center_x = total_screen_rect.x() + (total_screen_rect.width() - width) // 2
        center_y = total_screen_rect.y() + (total_screen_rect.height() - height) // 2
        print(f"Centering at: X={center_x}, Y={center_y}")  # More debugging output

Solution

  • You need to check the the screen geometries more carefully: according to your output, the joined rectangle starts at -1920, indicating a negative x in global coordinates: while the primary screen begins at 0, the leftmost one begins at -1920.

    But then you use the same geometry of the primary screen as a relative coordinate in order to get a reference position for the center, which is wrong: widget coordinates are always relative to the top left corner of the parent, which always is 0, 0, but your coordinate system should actually consider the whole geometry, which has negative global positions.

    In order to get the accurate position, you need to get the translated geometry of the primary screen using 0-based coordinates. Consider the following image:

    Scheme of screen geometries

    The simplified formula also shows why it could be applied for the left screen while obtaining the same result, since its geometry is at -1920:

    LeftScreen.x + LeftScreen.width / 2 - Desktop.x = Xcenter
        -1920    +     1920         / 2 -   -1920   = 960
    

    While you could simply add the width of the left screen, a more appropriate solution should work independently than the screen layout, so that it will always be consistent no matter if the overall screen geometry does start at 0, 0 or not: it may be negative, like in your case, but even positive, and the layout might also have a different vertical offset if you use screens with different heights or laid out vertically.

    Qt simplifies all this since it provides access functions to QRect corner points, meaning that we can simply translate the geometry to 0, 0 (widget) based coordinates using the negative of the top left corner: screen.translated(-desktop.topLeft()).

    # this may have an origin (top/left) different than 0, 0
    total_screen_rect = calculate_total_geometry()
    self.setGeometry(total_screen_rect)
    
    # get the origin point of the screen geometry
    origin = total_screen_rect.topLeft()
    
    primary_screen_geometry = QApplication.primaryScreen().geometry()
    
    # translate the geometry in "widget coordinates"; note the minus sign!
    local_screen_geometry = primary_screen_geometry.translated(-origin)
    center = local_screen_geometry.center()
    
    self.rubber_band = QRubberBand(QRubberBand.Rectangle, self)
    self.rubber_band.setGeometry(
        center.x() - width // 2, center.y() - height // 2, width, height)
    

    Note that there's no point in doing QRect(QPoint(x, y), QSize(w, h)), since Qt would internally do the same as QRect(x, y, w, h), making the creation of QPoint and QSize objects pointless; you could even skip the QRect creation for setGeometry() (as I did above) if you don't need that rectangle somewhere else, since Qt would create it anyway with the setGeometry(x, y, w, h) overload.

    Finally, you should call show() on a child widget only if the parent has been already shown.