Reputation: 1
I'm developing a painting application using PyQt6 where users can draw on images with a custom cursor. I'm experiencing an issue where there's a consistent offset between where my custom cursor appears and where the actual painting occurs. This offset is particularly noticeable at normal zoom levels but becomes imperceptible when highly zoomed in.
What I'm seeing:
My custom cursor creation code:
def create_cursor(self, size):
cursor_size = max(size * 2, 32)
cursor_pixmap = QPixmap(cursor_size, cursor_size)
cursor_pixmap.fill(Qt.GlobalColor.transparent)
painter = QPainter(cursor_pixmap)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
# Draw outer white circle
painter.setPen(QPen(Qt.GlobalColor.white, 2))
painter.drawEllipse(1, 1, cursor_size - 2, cursor_size - 2)
# Draw inner black circle
painter.setPen(QPen(Qt.GlobalColor.black, 1))
painter.drawEllipse(2, 2, cursor_size - 4, cursor_size - 4)
# Draw brush size indicator
painter.setPen(QPen(Qt.GlobalColor.red, 1, Qt.PenStyle.DotLine))
brush_circle_size = min(size, cursor_size - 4)
offset = (cursor_size - brush_circle_size) // 2
painter.drawEllipse(offset, offset, brush_circle_size, brush_circle_size)
# Draw crosshair
painter.setPen(QPen(Qt.GlobalColor.black, 1))
mid = cursor_size // 2
painter.drawLine(mid, 0, mid, cursor_size)
painter.drawLine(0, mid, cursor_size, mid)
hotspot = QPoint(cursor_size // 2, cursor_size // 2)
return QCursor(cursor_pixmap, hotspot.x(), hotspot.y())
def map_to_image(self, pos):
"""Maps window coordinates to image coordinates centering calculations."""
print("\n=== BEGIN CURSOR POSITION MAPPING DEBUG ===")
print(f"Raw input position: ({pos.x()}, {pos.y()})")
print(f"Current zoom factor: {self.zoom_factor}")
# Get scroll information
h_scroll = self.scroll_area.horizontalScrollBar()
v_scroll = self.scroll_area.verticalScrollBar()
h_scroll_visible = h_scroll.isVisible()
v_scroll_visible = v_scroll.isVisible()
scroll_x = h_scroll.value() if h_scroll_visible else 0
scroll_y = v_scroll.value() if v_scroll_visible else 0
print(
f"Scroll visibility - Horizontal: {h_scroll_visible}, Vertical: {v_scroll_visible}"
)
print(f"Scroll values - X: {scroll_x}, Y: {scroll_y}")
# Get image and viewport geometries
image_pos = self.image_mask_label.pos()
viewport_pos = self.scroll_area.viewport().pos()
image_rect = self.image_mask_label.rect()
viewport_rect = self.scroll_area.viewport().rect()
# Calculate the actual displayed size of the image (after zoom)
displayed_width = image_rect.width() # This is already scaled by zoom
displayed_height = image_rect.height() # This is already scaled by zoom
print(f"Image position: ({image_pos.x()}, {image_pos.y()})")
print(f"Viewport position: ({viewport_pos.x()}, {viewport_pos.y()})")
print(
f"Original Image dimensions: {self.mask_pixmap.width()}x{self.mask_pixmap.height()}"
)
print(f"Displayed Image dimensions: {displayed_width}x{displayed_height}")
print(f"Viewport dimensions: {viewport_rect.width()}x{viewport_rect.height()}")
# Calculate centering offsets based on displayed size vs viewport
offset_x = (
abs(viewport_rect.width() - displayed_width) // 2
if viewport_rect.width() > displayed_width
else 0
)
offset_y = (
abs(viewport_rect.height() - displayed_height) // 2
if viewport_rect.height() > displayed_height
else 0
)
print(f"Centering offsets - X: {offset_x}, Y: {offset_y}")
# Get cursor information
cursor = self.cursor()
hotspot = cursor.hotSpot()
print(f"Cursor hotspot: ({hotspot.x()}, {hotspot.y()})")
print(f"Current brush size: {self.brush_size}")
# Calculate position in image coordinates
if h_scroll_visible or v_scroll_visible:
# When scrollbars are visible, adjust for scroll position
screen_x = pos.x() + scroll_x - image_pos.x()
screen_y = pos.y() + scroll_y - image_pos.y()
print(f"Scroll-adjusted position: ({screen_x}, {screen_y})")
else:
# When no scrollbars, use centering offset
screen_x = pos.x() - offset_x - image_pos.x()
screen_y = pos.y() - offset_y - image_pos.y()
print(f"Offset-adjusted position: ({screen_x}, {screen_y})")
# Scale the position based on zoom
scaled_x = screen_x / self.zoom_factor
scaled_y = screen_y / self.zoom_factor
print(f"Zoom-scaled position: ({scaled_x}, {scaled_y})")
# Apply cursor centering correction
# Center the brush on the cursor by subtracting half the brush size
brush_offset = self.brush_size / 2
final_x = scaled_x - brush_offset
final_y = scaled_y - brush_offset
print(f"Brush-centered position: ({final_x}, {final_y})")
# Ensure coordinates are within image bounds
final_x = max(0, min(final_x, self.mask_pixmap.width() - 1))
final_y = max(0, min(final_y, self.mask_pixmap.height() - 1))
final_pos = QPoint(int(final_x), int(final_y))
print(f"Final mapped position: ({final_pos.x()}, {final_pos.y()})")
print(
f"Image boundaries: width={self.mask_pixmap.width()}, height={self.mask_pixmap.height()}"
)
print("=== END CURSOR POSITION MAPPING DEBUG ===\n")
return final_pos
def draw_point(self, pos):
if not self.mask_pixmap:
return
painter = QPainter(self.mask_pixmap)
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
# Create a temporary pixmap for the current stroke
temp_pixmap = QPixmap(self.mask_pixmap.size())
temp_pixmap.fill(Qt.GlobalColor.transparent)
temp_painter = QPainter(temp_pixmap)
temp_painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
# Set up the pen and brush
pen = QPen(
self.brush_color,
1,
Qt.PenStyle.SolidLine,
Qt.PenCapStyle.RoundCap,
Qt.PenJoinStyle.RoundJoin,
)
temp_painter.setPen(pen)
temp_painter.setBrush(QBrush(self.brush_color))
# Draw the stroke
if self.last_point:
temp_painter.setPen(
QPen(
self.brush_color,
self.brush_size,
Qt.PenStyle.SolidLine,
Qt.PenCapStyle.RoundCap,
Qt.PenJoinStyle.RoundJoin,
)
)
temp_painter.drawLine(self.last_point, pos)
else:
diameter = self.brush_size
top_left = QPoint(pos.x() - diameter // 2, pos.y() - diameter // 2)
temp_painter.drawEllipse(top_left.x(), top_left.y(), diameter, diameter)
temp_painter.end()
# Apply the stroke to the mask
if self.eraser_button.isChecked():
painter.setCompositionMode(
QPainter.CompositionMode.CompositionMode_DestinationOut
)
else:
painter.setCompositionMode(
QPainter.CompositionMode.CompositionMode_SourceOver
)
painter.drawPixmap(0, 0, temp_pixmap)
painter.end()
self.last_point = pos
self.update_display()
What I've tried:
I suspect this might be related to how Qt handles cursor positioning versus mouse event coordinates, but I can't figure out why the behavior changes with zoom level. Any ideas what could be causing this offset and why it behaves differently at different zoom levels?
Environment:
Python 3.10 PyQt6 Windows 11
Upvotes: 0
Views: 39