I have code that draws a rectangle on a widget using mouse press and drag. How can I make the left and right sides of that rectangle draggable afterwards, so I can adjust its width — similar to how you crop an image in photo editing software?
When you're building interactive graphics in PyQt6 — things like selection areas, crop boxes, or annotation tools — a common requirement is to let the user draw a rectangle and then fine-tune it afterwards. In this tutorial, you'll start with a simple rectangle-drawing widget and progressively add the ability to resize it by dragging its edges.
Drawing a basic rectangle
Let's start with the foundation: a widget that lets you click and drag to draw a rectangle.
import sys
from PyQt6.QtCore import QPoint, QRect
from PyQt6.QtGui import QBrush, QColor, QPainter
from PyQt6.QtWidgets import QApplication, QWidget
class MyWidget(QWidget):
def __init__(self):
super().__init__()
self.setGeometry(30, 30, 600, 400)
self.begin = QPoint()
self.end = QPoint()
def paintEvent(self, event):
qp = QPainter(self)
br = QBrush(QColor(100, 10, 10, 40))
qp.setBrush(br)
qp.drawRect(QRect(self.begin, self.end))
def mousePressEvent(self, event):
self.begin = event.pos()
self.end = event.pos()
self.update()
def mouseMoveEvent(self, event):
self.end = event.pos()
self.update()
def mouseReleaseEvent(self, event):
self.end = event.pos()
self.update()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MyWidget()
window.show()
sys.exit(app.exec())
This gives you a translucent red rectangle that appears as you click and drag. When you release the mouse, the rectangle stays in place. That's the starting point — now let's make those edges draggable.
Thinking in states
To support both drawing a new rectangle and resizing an existing one, the widget needs to know what mode it's in. When the user clicks and drags on an empty area, we draw a new rectangle. But when they click near the left or right edge of the existing rectangle, we want to move that edge instead.
We can represent these modes as simple constants:
FREE_STATE = 1
BUILDING_SQUARE = 2
BEGIN_SIDE_EDIT = 3
END_SIDE_EDIT = 4
FREE_STATE— no mouse button is pressed, the user is just moving the mouse around.BUILDING_SQUARE— the user is drawing a brand new rectangle.BEGIN_SIDE_EDIT— the user is dragging the begin side (the edge where the rectangle was first clicked).END_SIDE_EDIT— the user is dragging the end side (the opposite edge).
The widget starts in FREE_STATE. When the user presses the mouse button, we check whether they clicked near an existing edge. If so, we switch to the appropriate edit state. Otherwise, we start drawing a new rectangle. This state-based approach to handling mouse events builds on the fundamentals covered in Signals, Slots & Events.
Detecting clicks near an edge
How do we know if a click is "near" an edge? We compare the x-coordinate of the click to the x-coordinates of the rectangle's two vertical sides. If the difference is small (say, within 3 pixels), we treat it as a click on that edge.
We also need to make sure the click is vertically within the rectangle — clicking above or below the rectangle shouldn't trigger edge dragging.
Here's that check inside mousePressEvent:
def mousePressEvent(self, event):
if not self.begin.isNull() and not self.end.isNull():
p = event.pos()
y1, y2 = sorted([self.begin.y(), self.end.y()])
if y1 <= p.y() <= y2:
if abs(self.begin.x() - p.x()) <= 3:
self.state = BEGIN_SIDE_EDIT
return
elif abs(self.end.x() - p.x()) <= 3:
self.state = END_SIDE_EDIT
return
# If we didn't click on an edge, start a new rectangle
self.state = BUILDING_SQUARE
self.begin = event.pos()
self.end = event.pos()
self.update()
We use sorted() on the y-coordinates because the user might have drawn the rectangle from bottom to top, meaning begin.y() could be larger than end.y().
Applying the drag
Once we know the current state, we can route mouse movement to the right behavior. In a helper method called apply_event, we update the appropriate coordinate:
def apply_event(self, event):
if self.state == BUILDING_SQUARE:
self.end = event.pos()
elif self.state == BEGIN_SIDE_EDIT:
self.begin.setX(event.x())
elif self.state == END_SIDE_EDIT:
self.end.setX(event.x())
When resizing, we only change the x-coordinate of the relevant point. The y-coordinates stay the same, so the rectangle's height is preserved while the width changes.
Both mouseMoveEvent and mouseReleaseEvent call this same method:
def mouseMoveEvent(self, event):
self.apply_event(event)
self.update()
def mouseReleaseEvent(self, event):
self.apply_event(event)
self.state = FREE_STATE
When the mouse button is released, we reset back to FREE_STATE.
Putting it together: resizable rectangle
Here's the complete working example with edge dragging:
import sys
from PyQt6.QtCore import QPoint, QRect
from PyQt6.QtGui import QBrush, QColor, QPainter
from PyQt6.QtWidgets import QApplication, QWidget
FREE_STATE = 1
BUILDING_SQUARE = 2
BEGIN_SIDE_EDIT = 3
END_SIDE_EDIT = 4
class MyWidget(QWidget):
def __init__(self):
super().__init__()
self.setGeometry(30, 30, 600, 400)
self.begin = QPoint()
self.end = QPoint()
self.state = FREE_STATE
def paintEvent(self, event):
qp = QPainter(self)
br = QBrush(QColor(100, 10, 10, 40))
qp.setBrush(br)
qp.drawRect(QRect(self.begin, self.end))
def mousePressEvent(self, event):
if not self.begin.isNull() and not self.end.isNull():
p = event.pos()
y1, y2 = sorted([self.begin.y(), self.end.y()])
if y1 <= p.y() <= y2:
if abs(self.begin.x() - p.x()) <= 3:
self.state = BEGIN_SIDE_EDIT
return
elif abs(self.end.x() - p.x()) <= 3:
self.state = END_SIDE_EDIT
return
self.state = BUILDING_SQUARE
self.begin = event.pos()
self.end = event.pos()
self.update()
def apply_event(self, event):
if self.state == BUILDING_SQUARE:
self.end = event.pos()
elif self.state == BEGIN_SIDE_EDIT:
self.begin.setX(event.x())
elif self.state == END_SIDE_EDIT:
self.end.setX(event.x())
def mouseMoveEvent(self, event):
self.apply_event(event)
self.update()
def mouseReleaseEvent(self, event):
self.apply_event(event)
self.state = FREE_STATE
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MyWidget()
window.show()
sys.exit(app.exec())
Try running this. Draw a rectangle, then carefully position your mouse on the left or right edge and drag. You'll see the width change while the height stays fixed. If you click somewhere else entirely, a new rectangle is drawn.
Adding visual feedback
The basic version works, but it's hard to tell when your mouse is close enough to an edge to drag it. Good crop tools in photo editors give you visual hints — the cursor changes, and the edge highlights. Let's add that.
We need two things:
- Change the cursor to a horizontal resize arrow when the mouse hovers near an edge.
- Draw a dashed line on the edge that's about to be dragged.
To detect hover without clicking, we need to enable mouse tracking. By default, Qt only sends mouseMoveEvent while a button is pressed. Calling self.setMouseTracking(True) makes it fire all the time.
We also add a helper method cursor_on_side that returns which side (if any) the mouse is near. We use this both in mousePressEvent (to decide what to do on click) and in mouseMoveEvent (to update the visual feedback).
We'll also increase the hover detection threshold from 3 to 5 pixels, making it a bit easier to grab the edge.
Here's the complete example with visual feedback:
import sys
from PyQt6.QtCore import QPoint, QRect, Qt
from PyQt6.QtGui import QBrush, QColor, QPainter, QPen
from PyQt6.QtWidgets import QApplication, QWidget
FREE_STATE = 1
BUILDING_SQUARE = 2
BEGIN_SIDE_EDIT = 3
END_SIDE_EDIT = 4
CURSOR_ON_BEGIN_SIDE = 1
CURSOR_ON_END_SIDE = 2
class MyWidget(QWidget):
def __init__(self):
super().__init__()
self.setGeometry(30, 30, 600, 400)
self.begin = QPoint()
self.end = QPoint()
self.state = FREE_STATE
self.setMouseTracking(True)
self.hover_side = 0
def paintEvent(self, event):
qp = QPainter(self)
br = QBrush(QColor(100, 10, 10, 40))
qp.setBrush(br)
qp.drawRect(QRect(self.begin, self.end))
# Draw a dashed line on the hovered edge.
if not self.hover_side:
return
qp.setPen(QPen(Qt.GlobalColor.black, 5, Qt.PenStyle.DashLine))
if self.hover_side == CURSOR_ON_BEGIN_SIDE:
edge_end = QPoint(self.begin.x(), self.end.y())
qp.drawLine(self.begin, edge_end)
elif self.hover_side == CURSOR_ON_END_SIDE:
edge_start = QPoint(self.end.x(), self.begin.y())
qp.drawLine(edge_start, self.end)
def cursor_on_side(self, pos):
"""Return which side the cursor is near, or 0 if not near either."""
if self.begin.isNull() or self.end.isNull():
return 0
y1, y2 = sorted([self.begin.y(), self.end.y()])
if y1 <= pos.y() <= y2:
if abs(self.begin.x() - pos.x()) <= 5:
return CURSOR_ON_BEGIN_SIDE
elif abs(self.end.x() - pos.x()) <= 5:
return CURSOR_ON_END_SIDE
return 0
def mousePressEvent(self, event):
side = self.cursor_on_side(event.pos())
if side == CURSOR_ON_BEGIN_SIDE:
self.state = BEGIN_SIDE_EDIT
elif side == CURSOR_ON_END_SIDE:
self.state = END_SIDE_EDIT
else:
self.state = BUILDING_SQUARE
self.begin = event.pos()
self.end = event.pos()
self.update()
def apply_event(self, event):
if self.state == BUILDING_SQUARE:
self.end = event.pos()
elif self.state == BEGIN_SIDE_EDIT:
self.begin.setX(event.x())
elif self.state == END_SIDE_EDIT:
self.end.setX(event.x())
def mouseMoveEvent(self, event):
if self.state == FREE_STATE:
# Update hover feedback.
self.hover_side = self.cursor_on_side(event.pos())
if self.hover_side:
self.setCursor(Qt.CursorShape.SizeHorCursor)
else:
self.unsetCursor()
self.update()
else:
self.apply_event(event)
self.update()
def mouseReleaseEvent(self, event):
self.apply_event(event)
self.state = FREE_STATE
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MyWidget()
window.show()
sys.exit(app.exec())
Now when you hover near the left or right edge of the rectangle, the cursor changes to a horizontal resize arrow and a dashed line appears on that edge. This makes the interaction feel much more intuitive — the user can clearly see which edge they're about to drag before they click.
How it all fits together
Let's recap the flow:
-
Draw — Click and drag on an empty area to create a rectangle. The
stateisBUILDING_SQUARE, and bothbeginandendfollow the mouse. -
Hover — Move the mouse without clicking. If the cursor is near the left or right edge of the rectangle (within 5 pixels, and vertically within bounds), we show a dashed line and change the cursor.
-
Resize — Click on a highlighted edge and drag. Only the x-coordinate of that edge moves, keeping the rectangle's height fixed.
-
Release — Let go of the mouse. The state resets to
FREE_STATE, and the rectangle stays at its new size.
This pattern — using a state variable to track what the user is doing, and routing mouse events based on that state — is a very useful approach for any kind of interactive drawing or editing tool. You can extend it further to support dragging the top and bottom edges, moving the entire rectangle, or even adding corner handles for diagonal resizing. For more complex drawing scenarios, take a look at the QPainter bitmap graphics tutorial or the QGraphicsView vector graphics framework which provides built-in support for interactive, movable items. If you're interested in building your own reusable interactive widgets, our guide to creating custom widgets in PyQt6 covers the fundamentals.
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!