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:
(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.
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.
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!
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:
y_screen = height - 1 - y_math
Where:
y_screenis the y coordinate QPainter expects (origin at top-left).y_mathis the y coordinate in standard math notation (origin at bottom-left).heightis 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:
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:
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:
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).
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:
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:
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:
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.
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!