paint: Implement polygon copy using mask.

This commit is contained in:
Martin Fitzpatrick 2018-02-10 14:01:27 +01:00
parent c4e1edfa80
commit 0b722367e4
2 changed files with 112 additions and 61 deletions

View File

@ -1,15 +1,17 @@
# Minute Apps # 15 Minute Apps
A collection of minute (small) desktop applications written in Python A collection of 15 small (minute) desktop applications written in Python
using the PyQt framework. These apps are intended as examples from using the PyQt framework. These apps are intended as examples from
which you can poke, hack and prod your way to writing your own tools. which you can poke, hack and prod your way to writing your own tools.
If you find these apps interesting, or want to learn more about If you find these apps interesting, or want to learn more about
PyQt in general, [take a look at my ebook & online course PyQt in general, [take a look at my ebook & online course
"Create Simple GUI Applications"](https://martinfitzpatrick.name/create-simple-gui-applications) "Create Simple GUI Applications"](https://martinfitzpatrick.name/create-simple-gui-applications)
which covers everything you need to know to get you programming with PyQt. which covers everything you need to know to start building your own applications with PyQt.
More articles, tutorials and projects using PyQt can be
found [on my site](http://martinfitzpatrick.name/tag/pyqt).
Write-ups of these apps, including design explanations and walkthroughs, can be found [on my site]().
All code is **licensed under an MIT license**. This allows you to re-use the code freely, All code is **licensed under an MIT license**. This allows you to re-use the code freely,
remixed in both commercial and non-commercial projects. The only requirement is that you must remixed in both commercial and non-commercial projects. The only requirement is that you must

View File

@ -34,6 +34,8 @@ MODES = [
'ellipse', 'roundrect' 'ellipse', 'roundrect'
] ]
CANVAS_DIMENSIONS = 600, 400
STAMP_DIR = './stamps' STAMP_DIR = './stamps'
STAMPS = [os.path.join(STAMP_DIR, f) for f in os.listdir(STAMP_DIR)] STAMPS = [os.path.join(STAMP_DIR, f) for f in os.listdir(STAMP_DIR)]
@ -52,6 +54,7 @@ class Canvas(QLabel):
secondary_color_updated = pyqtSignal(str) secondary_color_updated = pyqtSignal(str)
active_color = None active_color = None
preview_pen = None
timer_event = None timer_event = None
@ -59,7 +62,7 @@ class Canvas(QLabel):
def reset(self): def reset(self):
# Create the pixmap for display. # Create the pixmap for display.
self.setPixmap(QPixmap(600,400)) self.setPixmap(QPixmap(*CANVAS_DIMENSIONS))
self.eraser_color = self.secondary_color or QColor(Qt.white) self.eraser_color = self.secondary_color or QColor(Qt.white)
self.eraser_color.setAlpha(100) self.eraser_color.setAlpha(100)
self.pixmap().fill(self.background_color) self.pixmap().fill(self.background_color)
@ -136,10 +139,86 @@ class Canvas(QLabel):
# Mode-specific events. # Mode-specific events.
# Select polygon events
def selectpoly_mousePressEvent(self, e):
if not self.locked or e.button == Qt.RightButton:
self.active_shape_fn = 'drawPolygon'
self.preview_pen = SELECTION_PEN
self.generic_poly_mousePressEvent(e)
def selectpoly_timerEvent(self, final=False):
self.generic_poly_timerEvent(final)
def selectpoly_mouseMoveEvent(self, e):
if not self.locked:
self.generic_poly_mouseMoveEvent(e)
def selectpoly_mouseDoubleClickEvent(self, e):
self.current_pos = e.pos()
self.locked = True
def selectpoly_copy(self):
"""
Copy a polygon region from the current image, returning it.
Create a mask for the selected area, and use it to blank
out non-selected regions. Then get the bounding rect of the
selection and crop to produce the smallest possible image.
:return: QPixmap of the copied region.
"""
self.timer_cleanup()
pixmap = self.pixmap().copy()
bitmap = QBitmap(*CANVAS_DIMENSIONS)
bitmap.clear() # Starts with random data visible.
p = QPainter(bitmap)
# Construct a mask where the user selected area will be kept, the rest removed from the image is transparent.
userpoly = QPolygon(self.history_pos + [self.current_pos])
p.setPen(QPen(Qt.color1))
p.setBrush(QBrush(Qt.color1)) # Solid color, Qt.color1 == bit on.
p.drawPolygon(userpoly)
p.end()
# Set our created mask on the image.
pixmap.setMask(bitmap)
# Calculate the bounding rect and return a copy of that region.
return pixmap.copy(userpoly.boundingRect())
# Select rectangle events
def selectrect_mousePressEvent(self, e):
self.active_shape_fn = 'drawRect'
self.preview_pen = SELECTION_PEN
self.generic_shape_mousePressEvent(e)
def selectrect_timerEvent(self, final=False):
self.generic_shape_timerEvent(final)
def selectrect_mouseMoveEvent(self, e):
if not self.locked:
self.current_pos = e.pos()
def selectrect_mouseReleaseEvent(self, e):
self.current_pos = e.pos()
self.locked = True
def selectrect_copy(self):
"""
Copy a rectangle region of the current image, returning it.
:return: QPixmap of the copied region.
"""
self.timer_cleanup()
return self.pixmap().copy(QRect(self.origin_pos, self.current_pos))
# Eraser events # Eraser events
def eraser_mousePressEvent(self, e): def eraser_mousePressEvent(self, e):
return self.generic_mousePressEvent(e) self.generic_mousePressEvent(e)
def eraser_mouseMoveEvent(self, e): def eraser_mouseMoveEvent(self, e):
if self.last_pos: if self.last_pos:
@ -151,7 +230,7 @@ class Canvas(QLabel):
self.update() self.update()
def eraser_mouseReleaseEvent(self, e): def eraser_mouseReleaseEvent(self, e):
return self.generic_mouseReleaseEvent(e) self.generic_mouseReleaseEvent(e)
# Stamp (pie) events # Stamp (pie) events
@ -164,43 +243,41 @@ class Canvas(QLabel):
# Pen events # Pen events
def pen_mousePressEvent(self, e): def pen_mousePressEvent(self, e):
return self.generic_mousePressEvent(e) self.generic_mousePressEvent(e)
def pen_mouseMoveEvent(self, e): def pen_mouseMoveEvent(self, e):
if self.last_pos: if self.last_pos:
p = QPainter(self.pixmap()) p = QPainter(self.pixmap())
p.setPen(QPen(self.active_color, 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)) p.setPen(QPen(self.active_color, 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
p.drawLine(self.last_pos, e.pos()) p.drawLine(self.last_pos, e.pos())
self.last_pos = e.pos() self.last_pos = e.pos()
self.update() self.update()
def pen_mouseReleaseEvent(self, e): def pen_mouseReleaseEvent(self, e):
return self.generic_mouseReleaseEvent(e) self.generic_mouseReleaseEvent(e)
# Brush events # Brush events
def brush_mousePressEvent(self, e): def brush_mousePressEvent(self, e):
return self.generic_mousePressEvent(e) self.generic_mousePressEvent(e)
def brush_mouseMoveEvent(self, e): def brush_mouseMoveEvent(self, e):
if self.last_pos: if self.last_pos:
p = QPainter(self.pixmap()) p = QPainter(self.pixmap())
p.setPen(QPen(self.active_color, 10, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)) p.setPen(QPen(self.active_color, 10, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
p.drawLine(self.last_pos, e.pos()) p.drawLine(self.last_pos, e.pos())
self.last_pos = e.pos() self.last_pos = e.pos()
self.update() self.update()
def brush_mouseReleaseEvent(self, e): def brush_mouseReleaseEvent(self, e):
return self.generic_mouseReleaseEvent(e) self.generic_mouseReleaseEvent(e)
# Spray events # Spray events
def spray_mousePressEvent(self, e): def spray_mousePressEvent(self, e):
return self.generic_mousePressEvent(e) self.generic_mousePressEvent(e)
def spray_mouseMoveEvent(self, e): def spray_mouseMoveEvent(self, e):
if self.last_pos: if self.last_pos:
@ -289,48 +366,7 @@ class Canvas(QLabel):
self.set_secondary_color(hex) self.set_secondary_color(hex)
self.secondary_color_updated.emit(hex) # Update UI. self.secondary_color_updated.emit(hex) # Update UI.
# Select rectangle events # Generic shape events: Rectangle, Ellipse, Rounded-rect
def selectrect_mousePressEvent(self, e):
if self.current_pos:
# Clear up indicator.
self.timer_cleanup()
self.reset_mode()
self.origin_pos = e.pos()
self.current_pos = e.pos()
self.timer_event = self.selectrect_timerEvent
def selectrect_timerEvent(self, final=False):
p = QPainter(self.pixmap())
p.setCompositionMode(QPainter.RasterOp_SourceXorDestination)
pen = QPen(QColor(0xff, 0xff, 0xff), 1, Qt.DashLine)
pen.setDashOffset(self.dash_offset)
p.setPen(pen)
if self.last_pos:
p.drawRect(QRect(self.origin_pos, self.last_pos))
self.dash_offset -= 1
if not final:
pen.setDashOffset(self.dash_offset)
p.setPen(pen)
p.drawRect(QRect(self.origin_pos, self.current_pos))
self.update()
self.last_pos = self.current_pos
def selectrect_mouseMoveEvent(self, e):
if self.locked == False:
self.current_pos = e.pos()
def selectrect_mouseReleaseEvent(self, e):
self.current_pos = e.pos()
self.locked = True
def selectrect_copy(self):
return self.pixmap().copy(QRect(self.origin_pos, self.current_pos))
# Generic shape events: Rectangle & Ellipse
def generic_shape_mousePressEvent(self, e): def generic_shape_mousePressEvent(self, e):
self.origin_pos = e.pos() self.origin_pos = e.pos()
@ -340,12 +376,16 @@ class Canvas(QLabel):
def generic_shape_timerEvent(self, final=False): def generic_shape_timerEvent(self, final=False):
p = QPainter(self.pixmap()) p = QPainter(self.pixmap())
p.setCompositionMode(QPainter.RasterOp_SourceXorDestination) p.setCompositionMode(QPainter.RasterOp_SourceXorDestination)
pen = PREVIEW_PEN pen = self.preview_pen
pen.setDashOffset(self.dash_offset)
p.setPen(pen) p.setPen(pen)
if self.last_pos: if self.last_pos:
getattr(p, self.active_shape_fn)(QRect(self.origin_pos, self.last_pos), *self.active_shape_args) getattr(p, self.active_shape_fn)(QRect(self.origin_pos, self.last_pos), *self.active_shape_args)
if not final: if not final:
self.dash_offset -= 1
pen.setDashOffset(self.dash_offset)
p.setPen(pen)
getattr(p, self.active_shape_fn)(QRect(self.origin_pos, self.current_pos), *self.active_shape_args) getattr(p, self.active_shape_fn)(QRect(self.origin_pos, self.current_pos), *self.active_shape_args)
self.update() self.update()
@ -369,18 +409,18 @@ class Canvas(QLabel):
self.reset_mode() self.reset_mode()
# Line events # Line events
def line_mousePressEvent(self, e): def line_mousePressEvent(self, e):
self.origin_pos = e.pos() self.origin_pos = e.pos()
self.current_pos = e.pos() self.current_pos = e.pos()
self.preview_pen = PREVIEW_PEN
self.timer_event = self.line_timerEvent self.timer_event = self.line_timerEvent
def line_timerEvent(self, final=False): def line_timerEvent(self, final=False):
p = QPainter(self.pixmap()) p = QPainter(self.pixmap())
p.setCompositionMode(QPainter.RasterOp_SourceXorDestination) p.setCompositionMode(QPainter.RasterOp_SourceXorDestination)
pen = PREVIEW_PEN pen = self.preview_pen
p.setPen(pen) p.setPen(pen)
if self.last_pos: if self.last_pos:
p.drawLine(self.origin_pos, self.last_pos) p.drawLine(self.origin_pos, self.last_pos)
@ -417,7 +457,7 @@ class Canvas(QLabel):
self.current_pos = e.pos() self.current_pos = e.pos()
self.timer_event = self.generic_poly_timerEvent self.timer_event = self.generic_poly_timerEvent
if e.button() == Qt.RightButton and self.history_pos: elif e.button() == Qt.RightButton and self.history_pos:
# Clean up, we're not drawing # Clean up, we're not drawing
self.timer_cleanup() self.timer_cleanup()
self.reset_mode() self.reset_mode()
@ -425,12 +465,16 @@ class Canvas(QLabel):
def generic_poly_timerEvent(self, final=False): def generic_poly_timerEvent(self, final=False):
p = QPainter(self.pixmap()) p = QPainter(self.pixmap())
p.setCompositionMode(QPainter.RasterOp_SourceXorDestination) p.setCompositionMode(QPainter.RasterOp_SourceXorDestination)
pen = PREVIEW_PEN pen = self.preview_pen
pen.setDashOffset(self.dash_offset)
p.setPen(pen) p.setPen(pen)
if self.last_pos: if self.last_pos:
getattr(p, self.active_shape_fn)(*self.history_pos + [self.last_pos]) getattr(p, self.active_shape_fn)(*self.history_pos + [self.last_pos])
if not final: if not final:
self.dash_offset -= 1
pen.setDashOffset(self.dash_offset)
p.setPen(pen)
getattr(p, self.active_shape_fn)(*self.history_pos + [self.current_pos]) getattr(p, self.active_shape_fn)(*self.history_pos + [self.current_pos])
self.update() self.update()
@ -456,6 +500,7 @@ class Canvas(QLabel):
def polyline_mousePressEvent(self, e): def polyline_mousePressEvent(self, e):
self.active_shape_fn = 'drawPolyline' self.active_shape_fn = 'drawPolyline'
self.preview_pen = PREVIEW_PEN
self.generic_poly_mousePressEvent(e) self.generic_poly_mousePressEvent(e)
def polyline_timerEvent(self, final=False): def polyline_timerEvent(self, final=False):
@ -472,6 +517,7 @@ class Canvas(QLabel):
def rect_mousePressEvent(self, e): def rect_mousePressEvent(self, e):
self.active_shape_fn = 'drawRect' self.active_shape_fn = 'drawRect'
self.active_shape_args = () self.active_shape_args = ()
self.preview_pen = PREVIEW_PEN
self.generic_shape_mousePressEvent(e) self.generic_shape_mousePressEvent(e)
def rect_timerEvent(self, final=False): def rect_timerEvent(self, final=False):
@ -487,6 +533,7 @@ class Canvas(QLabel):
def polygon_mousePressEvent(self, e): def polygon_mousePressEvent(self, e):
self.active_shape_fn = 'drawPolygon' self.active_shape_fn = 'drawPolygon'
self.preview_pen = PREVIEW_PEN
self.generic_poly_mousePressEvent(e) self.generic_poly_mousePressEvent(e)
def polygon_timerEvent(self, final=False): def polygon_timerEvent(self, final=False):
@ -503,6 +550,7 @@ class Canvas(QLabel):
def ellipse_mousePressEvent(self, e): def ellipse_mousePressEvent(self, e):
self.active_shape_fn = 'drawEllipse' self.active_shape_fn = 'drawEllipse'
self.active_shape_args = () self.active_shape_args = ()
self.preview_pen = PREVIEW_PEN
self.generic_shape_mousePressEvent(e) self.generic_shape_mousePressEvent(e)
def ellipse_timerEvent(self, final=False): def ellipse_timerEvent(self, final=False):
@ -519,6 +567,7 @@ class Canvas(QLabel):
def roundrect_mousePressEvent(self, e): def roundrect_mousePressEvent(self, e):
self.active_shape_fn = 'drawRoundedRect' self.active_shape_fn = 'drawRoundedRect'
self.active_shape_args = (25, 25) self.active_shape_args = (25, 25)
self.preview_pen = PREVIEW_PEN
self.generic_shape_mousePressEvent(e) self.generic_shape_mousePressEvent(e)
def roundrect_timerEvent(self, final=False): def roundrect_timerEvent(self, final=False):