PyQt6绘图应用:自定义光标与实际绘制位置偏移问题排查
自定义光标与绘制位置偏移问题(PyQt6绘图应用)
问题背景
我用PyQt6开发绘图应用,用户可通过自定义光标在图像上绘制,但自定义光标显示位置和实际绘制位置始终存在偏移——100%缩放时偏移明显,高缩放比例(如200%)下几乎不可见。
现象说明
- 100%缩放时,光标中心与绘制的线条/点之间存在间隙
- 200%缩放时,绘制位置几乎与光标对齐
- 线条并非从光标中心起始
相关代码
自定义光标创建代码
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()
已尝试的解决方法
- 调整光标创建时的hotspot位置
- 修改笔刷偏移计算逻辑
- 当前坐标转换效果最优,但仍不准确
- 调试光标与绘制位置(两者单独查看均显示正确)
疑问
怀疑问题与Qt处理光标定位和鼠标事件坐标的方式有关,但无法理解为何偏移情况会随缩放比例变化。请问可能是什么原因导致该偏移,以及为何其表现会随缩放级别不同而改变?
环境信息
- Python 3.10
- PyQt6
- Windows 11
问题原因分析
- 双重偏移叠加:
map_to_image中已经减去了笔刷半尺寸的偏移,而draw_point的单点绘制逻辑里又再次计算了笔刷左上角的偏移,相当于做了两次中心偏移,直接导致绘制位置偏离光标热点。 - 精度丢失:
map_to_image中将浮点坐标直接取整为整数QPoint,100%缩放时这个取整误差会直接反映在绘制结果上;高缩放比例下,缩放后的坐标数值更大,取整误差占比极小,因此偏移不明显。 - 坐标基准错误:鼠标事件的
pos()默认是接收事件部件的局部坐标,若你传入的是全局坐标却用局部坐标的逻辑计算,会导致映射偏差。
解决方法
1. 移除重复偏移逻辑
删除map_to_image中的笔刷偏移代码,将绘制偏移统一放在draw_point中:
# 移除map_to_image内以下代码 # brush_offset = self.brush_size / 2 # final_x = scaled_x - brush_offset # final_y = scaled_y - brush_offset
2. 保留浮点精度
修改map_to_image返回浮点坐标,避免过早取整:
# 修改map_to_image的返回部分 final_x = max(0.0, min(scaled_x, self.mask_pixmap.width() - 1.0)) final_y = max(0.0, min(scaled_y, self.mask_pixmap.height() - 1.0)) return QPointF(final_x, final_y)
同时更新draw_point以支持浮点坐标绘制:
def draw_point(self, pos: QPointF): # ... 其他代码不变 if not self.last_point: diameter = self.brush_size # 使用浮点坐标计算椭圆中心 temp_painter.drawEllipse(pos, diameter / 2, diameter / 2) # ... 其他代码不变
3. 统一坐标基准
确保传入map_to_image的是部件局部坐标,若使用全局坐标需先转换:
# 鼠标事件处理中转换坐标 local_pos = self.image_mask_label.mapFromGlobal(event.globalPos()) image_pos = self.map_to_image(local_pos)
内容的提问来源于stack exchange,提问作者Kinjalk Parth
相关产品推荐
相关产品推荐

