Understanding QPainter Coordinates in PyQt6

How the coordinate system works for drawing on canvases in PyQt6
Heads up! You've already completed this tutorial.

I really having trouble understanding the coordinate system used in QPainter. Can you explain how this works?

If you've started drawing with QPainter in PyQt6, you might have been surprised the first time you drew a line. You pass in coordinates like (10, 10, 300, 200) and the result doesn't look quite like what you'd expect from a math class. That's because QPainter uses a coordinate system where the origin (0, 0) is in the top-left corner of the canvas, not the bottom-left.

This catches a lot of people off guard, so in this tutorial we'll walk through exactly how QPainter coordinates work, how to visualize them, and how to convert between screen coordinates and the mathematical coordinate system you might be more familiar with.

The QPainter coordinate system

In most math courses, you learn to plot points on a Cartesian plane where (0, 0) is at the bottom-left. The x-axis increases to the right, and the y-axis increases upward.

QPainter (and most screen-based graphics systems) does things differently:

  • (0, 0) is at the top-left corner of the drawing surface.
  • The x-axis increases to the right (same as math).
  • The y-axis increases downward (opposite of math).

This means that as your y value gets larger, you move down the screen, not up. Here's a simple diagram to illustrate:

python
(0,0) ───────────────► x increases
  │
  │
  │
  │
  ▼
  y increases

So when you call painter.drawLine(10, 10, 300, 200), you're drawing a line from a point near the top-left corner down to a point further right and further down the canvas.

Seeing it in action

Let's draw a line and annotate the start and end points so you can see exactly where the coordinates land. This complete example creates a small window with a QLabel displaying a QPixmap that we draw onto.

python
import sys

from PyQt6.QtCore import Qt
from PyQt6.QtGui import QPixmap, QPainter, QPen, QFont
from PyQt6.QtWidgets import QApplication, QLabel, QMainWindow


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("QPainter Coordinates")

        canvas = QPixmap(400, 300)
        canvas.fill(Qt.white)

        painter = QPainter(canvas)

        # Draw the line.
        pen = QPen(Qt.blue, 2)
        painter.setPen(pen)
        painter.drawLine(10, 10, 300, 200)

        # Annotate the start point.
        pen = QPen(Qt.red, 6)
        painter.setPen(pen)
        painter.drawPoint(10, 10)

        painter.setPen(QPen(Qt.black))
        painter.setFont(QFont("Arial", 10))
        painter.drawText(20, 15, "(10, 10)")

        # Annotate the end point.
        pen = QPen(Qt.red, 6)
        painter.setPen(pen)
        painter.drawPoint(300, 200)

        painter.setPen(QPen(Qt.black))
        painter.drawText(220, 220, "(300, 200)")

        painter.end()

        label = QLabel()
        label.setPixmap(canvas)
        self.setCentralWidget(label)


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()

Run this and you'll see a blue line drawn from near the top-left corner of the canvas down to a point lower and to the right. The red dots and labels mark each endpoint, making it clear that (10, 10) is near the top-left and (300, 200) is toward the bottom-right.

QPainter coordinates with annotated points

Create GUI Applications with Python & Qt6 by Martin Fitzpatrick — (PyQt6 Edition) The hands-on guide to making apps with Python — Over 15,000 copies sold!

More info Get the book

This is the expected behavior — the y-axis points downward.

Why does it work this way?

Screen coordinate systems with the origin at the top-left are a convention inherited from early computer displays, where the electron beam in a CRT monitor scanned from the top-left of the screen, line by line, downward. This convention carried forward into virtually all modern windowing and graphics systems, including Qt.

Converting from mathematical coordinates

If you're working with data that uses standard mathematical coordinates (origin at the bottom-left, y increasing upward), you'll need to convert the y values before drawing. The formula is straightforward:

python
y_screen = height - 1 - y_math

Where:

  • y_screen is the y coordinate QPainter expects (origin at top-left).
  • y_math is the y coordinate in standard math notation (origin at bottom-left).
  • height is the height of your drawing surface in pixels.

The - 1 is there because pixel coordinates are zero-indexed. A QPixmap with a height of 300 has valid y coordinates from 0 to 299.

Let's say you have a canvas that's 300 pixels tall, and you want to draw a line from the mathematical point (10, 10) to (300, 200) as if the origin were at the bottom-left. You'd convert each y coordinate:

python
height = 300

# Mathematical coordinates.
x1, y1_math = 10, 10
x2, y2_math = 300, 200

# Convert y values for screen drawing.
y1_screen = height - 1 - y1_math  # 300 - 1 - 10 = 289
y2_screen = height - 1 - y2_math  # 300 - 1 - 200 = 99

painter.drawLine(x1, y1_screen, x2, y2_screen)
# Equivalent to: painter.drawLine(10, 289, 300, 99)

Now the line will go from near the bottom-left upward to the right — just like you'd expect on a math plot.

A helper function for coordinate conversion

If you're doing a lot of drawing with mathematical coordinates, a small helper function keeps things tidy:

python
def math_to_screen(x, y, height):
    """Convert mathematical (bottom-left origin) coordinates
    to screen (top-left origin) coordinates."""
    return x, height - 1 - y

You can then use it like this:

python
x1, y1 = math_to_screen(10, 10, canvas_height)
x2, y2 = math_to_screen(300, 200, canvas_height)
painter.drawLine(x1, y1, x2, y2)

Comparing both coordinate systems side by side

This complete example draws the same line using both coordinate systems, so you can see the difference clearly. The left canvas uses QPainter's native coordinates (origin top-left), and the right canvas converts from mathematical coordinates (origin bottom-left).

python
import sys

from PyQt6.QtCore import Qt
from PyQt6.QtGui import QPixmap, QPainter, QPen, QFont
from PyQt6.QtWidgets import (
    QApplication, QLabel, QMainWindow, QHBoxLayout, QVBoxLayout, QWidget,
)


def math_to_screen(x, y, height):
    """Convert mathematical (bottom-left origin) coordinates
    to screen (top-left origin) coordinates."""
    return x, height - 1 - y


def draw_annotated_line(canvas, x1, y1, x2, y2, label_start, label_end):
    """Draw a line on a QPixmap with annotated endpoints."""
    painter = QPainter(canvas)

    # Draw the line.
    pen = QPen(Qt.blue, 2)
    painter.setPen(pen)
    painter.drawLine(x1, y1, x2, y2)

    # Draw and label the start point.
    painter.setPen(QPen(Qt.red, 6))
    painter.drawPoint(x1, y1)
    painter.setPen(QPen(Qt.black))
    painter.setFont(QFont("Arial", 9))
    painter.drawText(x1 + 8, y1 + 5, label_start)

    # Draw and label the end point.
    painter.setPen(QPen(Qt.red, 6))
    painter.drawPoint(x2, y2)
    painter.setPen(QPen(Qt.black))
    painter.drawText(x2 - 80, y2 + 20, label_end)

    painter.end()


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Coordinate System Comparison")

        canvas_width = 350
        canvas_height = 300

        # --- Left canvas: native QPainter coordinates ---
        canvas_native = QPixmap(canvas_width, canvas_height)
        canvas_native.fill(Qt.white)
        draw_annotated_line(
            canvas_native,
            10, 10, 300, 200,
            "(10, 10)", "(300, 200)",
        )

        label_native = QLabel()
        label_native.setPixmap(canvas_native)

        title_native = QLabel("Screen coordinates\n(origin top-left)")
        title_native.setAlignment(Qt.AlignCenter)
        title_native.setStyleSheet("font-weight: bold;")

        left_layout = QVBoxLayout()
        left_layout.addWidget(title_native)
        left_layout.addWidget(label_native)

        # --- Right canvas: mathematical coordinates converted ---
        canvas_math = QPixmap(canvas_width, canvas_height)
        canvas_math.fill(Qt.white)

        sx1, sy1 = math_to_screen(10, 10, canvas_height)
        sx2, sy2 = math_to_screen(300, 200, canvas_height)
        draw_annotated_line(
            canvas_math,
            sx1, sy1, sx2, sy2,
            f"math(10,10) → screen({sx1},{sy1})",
            f"math(300,200) → screen({sx2},{sy2})",
        )

        label_math = QLabel()
        label_math.setPixmap(canvas_math)

        title_math = QLabel("Math coordinates converted\n(origin bottom-left)")
        title_math.setAlignment(Qt.AlignCenter)
        title_math.setStyleSheet("font-weight: bold;")

        right_layout = QVBoxLayout()
        right_layout.addWidget(title_math)
        right_layout.addWidget(label_math)

        # --- Combine both sides ---
        main_layout = QHBoxLayout()
        main_layout.addLayout(left_layout)
        main_layout.addLayout(right_layout)

        container = QWidget()
        container.setLayout(main_layout)
        self.setCentralWidget(container)


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()

When you run this, you'll see two canvases side by side. On the left, the line slopes downward from the top-left, which is what QPainter naturally produces. On the right, the same mathematical coordinates have been converted, so the line slopes upward from the bottom-left — matching what you'd see on a standard math plot.

Drawing axes to orient yourself

When you're experimenting with coordinates, it can help to draw a simple set of axes on your canvas. Here's a quick helper that draws x and y axes with the origin marked:

python
import sys

from PyQt6.QtCore import Qt
from PyQt6.QtGui import QPixmap, QPainter, QPen, QFont
from PyQt6.QtWidgets import QApplication, QLabel, QMainWindow


def draw_axes(painter, width, height):
    """Draw simple x and y axes with labels."""
    painter.setPen(QPen(Qt.gray, 1, Qt.DashLine))

    # X-axis along the top (y=0).
    painter.drawLine(0, 0, width - 1, 0)

    # Y-axis along the left (x=0).
    painter.drawLine(0, 0, 0, height - 1)

    # Label the origin.
    painter.setPen(QPen(Qt.darkGray))
    painter.setFont(QFont("Arial", 8))
    painter.drawText(5, 15, "(0, 0)")

    # Label the x direction.
    painter.drawText(width - 60, 15, f"x → ({width - 1})")

    # Label the y direction.
    painter.save()
    painter.translate(15, height - 10)
    painter.drawText(0, 0, f"y ↓ ({height - 1})")
    painter.restore()


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("QPainter Axes")

        canvas_width = 400
        canvas_height = 300

        canvas = QPixmap(canvas_width, canvas_height)
        canvas.fill(Qt.white)

        painter = QPainter(canvas)
        draw_axes(painter, canvas_width, canvas_height)

        # Draw some points to see where they land.
        points = [
            (50, 50),
            (200, 150),
            (350, 250),
            (350, 50),
            (50, 250),
        ]

        painter.setPen(QPen(Qt.red, 6))
        for x, y in points:
            painter.drawPoint(x, y)

        painter.setPen(QPen(Qt.black))
        painter.setFont(QFont("Arial", 9))
        for x, y in points:
            painter.drawText(x + 6, y - 6, f"({x}, {y})")

        painter.end()

        label = QLabel()
        label.setPixmap(canvas)
        self.setCentralWidget(label)


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()

This draws the axes along the top and left edges of the canvas and plots several points with their coordinates labeled. It's a great way to build intuition about where things will appear.

Valid coordinate ranges

One more thing to keep in mind: pixel coordinates on a QPixmap are zero-indexed. If you create a pixmap with:

python
canvas = QPixmap(400, 300)

Then the valid coordinate ranges are:

  • x: 0 to 399 (that's width - 1)
  • y: 0 to 299 (that's height - 1)

Drawing outside these ranges won't cause an error, but anything beyond the edges simply won't be visible.

Summary

The QPainter coordinate system places (0, 0) at the top-left of the drawing surface, with x increasing to the right and y increasing downward. This is standard across virtually all screen-based graphics systems.

If you need to work with mathematical coordinates where (0, 0) is at the bottom-left and y increases upward, you can convert using the formula:

python
y_screen = height - 1 - y_math

Once you've internalized this, drawing with QPainter becomes predictable. When in doubt, drop some annotated points on your canvas — seeing the coordinates labeled right next to the dots is the fastest way to confirm everything is landing where you expect.

For more details on Qt's coordinate system, take a look at the official Qt coordinate system documentation.

1:1 Coaching & Tutoring for your Python GUIs project
Martin Fitzpatrick Python GUIs Coaching & Training
60 mins ($195) Book Now

1:1 Python GUIs Coaching & Training

Comprehensive code reviewBugfixes & improvements • Maintainability advice and architecture improvements • Design and usability assessment • Suggestions and tips to expand your knowledgePackaging and distribution help for Windows, Mac & Linux • Find out more.

Well done, you've finished this tutorial! Mark As Complete
[[ user.completed.length ]] completed [[ user.streak+1 ]] day streak

Create GUI Applications with Python & Qt6 by Martin Fitzpatrick

(PyQt6 Edition) The hands-on guide to making apps with Python — Over 15,000 copies sold!

More info Get the book

Martin Fitzpatrick

Understanding QPainter Coordinates in PyQt6 was written by Martin Fitzpatrick.

Martin Fitzpatrick has been developing Python/Qt apps for 8 years. Building desktop applications to make data-analysis tools more user-friendly, Python was the obvious choice. Starting with Tk, later moving to wxWidgets and finally adopting PyQt. Martin founded PythonGUIs to provide easy to follow GUI programming tutorials to the Python community. He has written a number of popular Python books on the subject.