1

I have this Ruler class

Right now the scale looks like the below image. I want the ticks to have their roots towards the view instead of having them outwards. The base of the scale should be inside.

I am not able to get the roots to the scales inner border(the black line).

Here is my code for the class:

class Ruler(QWidget):
    def __init__(self, orientation=Qt.Orientation.Horizontal, parent=None):
        super().__init__(parent)
        self._orientation = orientation
        self._origin = 0.0
        self._unit = 1.0
        self._zoom = 1.0
        self._mouse_tracking = False
        self._draw_text = False
        self._cursor_pos = QPoint()
        self.setMouseTracking(True)
        f = QFont("Courier", 9)
        f.setStyleHint(QFont.StyleHint.TypeWriter)
        self.setFont(f)

        # Ensure room for labels inside the ruler
        if orientation == Qt.Orientation.Horizontal:
            self.setFixedHeight(RULER_BREADTH + 5)
        else:
            self.setFixedWidth(RULER_BREADTH + 5)

    def minimumSizeHint(self):
        return self.sizeHint()  # We'll constrain via layout if needed

    def sizeHint(self):
        if self._orientation == Qt.Orientation.Horizontal:
            return super().sizeHint().expandedTo(self.minimumSizeHint()).boundedTo(self.minimumSizeHint())
        return super().sizeHint().expandedTo(self.minimumSizeHint()).boundedTo(self.minimumSizeHint())


    def paintEvent(self, event):
        painter = QPainter(self)
        painter.setRenderHints(
            QPainter.RenderHint.Antialiasing |
            QPainter.RenderHint.TextAntialiasing
        )

        # Fill background
        painter.fillRect(self.rect(), QColor(236, 233, 216))

        # Minor ticks: every 1 mm, length = 1/3 of breadth
        self._draw_scale(painter, mm_interval=1,
                         tick_length=RULER_BREADTH / 3,
                         draw_labels=False)

        # Medium ticks: every 5 mm, length = 2/3 of breadth
        self._draw_scale(painter, mm_interval=5,
                         tick_length=RULER_BREADTH * 2 / 3,
                         draw_labels=False)

        # Major ticks: every 10 mm, full length, with labels
        self._draw_scale(painter, mm_interval=10,
                         tick_length=RULER_BREADTH,
                         draw_labels=True)

        # Draw mouse position indicator
        if self._mouse_tracking:
            painter.setOpacity(0.4)
            # assume _draw_mouse_tick exists
            self._draw_mouse_tick(painter)
            painter.setOpacity(1.0)

        # Draw border line
        painter.setPen(QPen(Qt.GlobalColor.black, 2))
        if self._orientation == Qt.Orientation.Horizontal:
            painter.drawLine(self.rect().bottomLeft(),
                             self.rect().bottomRight())
        else:
            painter.drawLine(self.rect().topRight(),
                             self.rect().bottomRight())


    def _draw_scale(self, painter, mm_interval, tick_length, draw_labels=False):
        is_horz = (self._orientation == Qt.Orientation.Horizontal)
        length  = self.width() if is_horz else self.height()

        # Compute pixels-per-mm from DPI
        screen = QApplication.primaryScreen()
        dpi    = (screen.logicalDotsPerInchX() if is_horz
                  else screen.logicalDotsPerInchY())
        px_per_mm = dpi / 25.4
        step_pix  = mm_interval * px_per_mm * self._zoom

        # Determine visible tick indices
        first_tick = int(self._origin // step_pix) - 1
        last_tick  = int((self._origin + length) // step_pix) + 1

        for tick in range(first_tick, last_tick + 1):
            scene_pos  = tick * step_pix
            screen_pos = scene_pos - self._origin
            if screen_pos < 0 or screen_pos > length:
                continue

            # Draw the tick line
            painter.setPen(QPen(Qt.GlobalColor.red, 1))
            if is_horz:
                
                painter.drawLine(
                    QPointF(screen_pos, 0),
                    QPointF(screen_pos, tick_length)
                )
                
            else:
                painter.drawLine(
                    QPointF(0, screen_pos),
                    QPointF(tick_length, screen_pos)
                )

            # Draw the label inside the ruler
            if draw_labels and (tick % (10 // mm_interval) == 0):
                label = str(abs(tick * mm_interval))
                painter.setPen(QPen(Qt.GlobalColor.black))
                if is_horz:
                    x = int(screen_pos + 2)
                    y = int(tick_length - 10)
                    painter.drawText(x, y, label)
                else:
                    painter.save()
                    tx = tick_length - 2
                    ty = screen_pos + 2
                    painter.translate(tx, ty)
                    painter.rotate(-90)
                    painter.drawText(0, 0, label)
                    painter.restore()
       
    
    
    def _draw_mouse_tick(self, painter):
        if self._orientation == Qt.Orientation.Horizontal:
            x = self._cursor_pos.x()
            painter.drawLine(QPointF(x, 0), QPointF(x, self.height()))
        else:
            y = self._cursor_pos.y()
            painter.drawLine(QPointF(0, y), QPointF(self.width(), y))

    def mouseMoveEvent(self, event):
        self._cursor_pos = event.position().toPoint()
        self.update()
        super().mouseMoveEvent(event)

    def setOrigin(self, val):
        self._origin = val
        self.update()

    def setUnit(self, val):
        self._unit = val
        self.update()

    def setZoom(self, val):
        self._zoom = val
        self.update()

    def setMouseTrack(self, track: bool):
        self._mouse_tracking = track
        self.update()

    @staticmethod
    def toMM(orientation: Qt.Orientation) -> float:
        screen = QApplication.primaryScreen()
        if orientation == Qt.Orientation.Horizontal:
            dpi = screen.logicalDotsPerInchX()
        else:
            dpi = screen.logicalDotsPerInchY()
        # 1 inch = 25.4 mm → mm per pixel = 25.4 mm / (dots per inch)
        return 25.4 / dpi

Can anyone please help me solve the issue.

If could specify the direction of the ruler's base dynamically that would be better.

Thank you

: scale screenshot

3
  • 2
    You could simply replace the 0 and tick_length of the line points with self.height() and self.height() - tick_length (or width() for the vertical orientation). Note that the sizeHint() implementation will cause a recursion (because you're calling minimumSizeHint(), which you overrode by returning again your sizeHint()), and that expandedTo / boundedTo combination is pointless; besides, QWidget has invalid size hints (both return QSize(-1, -1)) if no layout is set, so it's up to you to eventually return appropriate sizes. Commented Aug 25 at 15:31
  • Thank you, that solved my issue. If you can post it as an answer I will accept that. Commented Aug 29 at 8:34
  • You can do it by yourself by clicking the "Answer your question" button. Just ensure that you add a brief but exhaustive explanation of what you did, and include appropriate code changes based on your implementation. Also, remember that transformations can be expansive, and it's usually better to "group" them whenever possible so that painting doesn't block event handling unnecessarily: try to improve your code by first draw all lines, and then draw text after the transformation (if any), instead of continuously switching the painter status. Commented Aug 30 at 3:53

0

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.