May-11-2021, 05:35 AM
(This post was last modified: May-11-2021, 01:04 PM by deanhystad.)
This is an app written with Qt. It uses a custom button that draws an image as the button background. Multiple tile buttons create a TileControl that uses a signal to call a function when one of the image buttons is pressed.
"""Mosaic image generator""" import sys import math import pathlib from functools import partial from PIL import Image, ImageQt from PySide2.QtWidgets import QApplication, QPushButton, QWidget, QDialog, QGridLayout, QVBoxLayout from PySide2.QtGui import QPainter, QPainterPath, QCursor from PySide2.QtCore import Qt, QRect, Signal TILE_SIZE = (60, 60) TILE_RECT = QRect(2, 2, 56, 56) class Tile(QPushButton): """A button for displaying images""" def __init__(self, parent): super().__init__(parent) self.setFixedSize(*TILE_SIZE) self.image = None self.image_file = None def paintEvent(self, _): """Paint widget. Fill with image or draw a rounded rectangle""" painter = QPainter(self) if self.image: painter.drawImage(0, 0, ImageQt.ImageQt(self.image)) else: path = QPainterPath() path.addRoundedRect(TILE_RECT, 4, 4) painter.fillPath(path, Qt.lightGray) painter.end() def load_image(self, image): """Load image from a file""" self.image_file = image if self.image_file: self.image = Image.open(image).resize(TILE_SIZE) else: self.image = None self.update() class TileControl(QWidget): """A matrix of tiles. Use clicked signal to be notified when a tile is selected""" clicked = Signal(Tile) def __init__(self, parent, rows, columns): super().__init__(parent) layout = QGridLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setHorizontalSpacing(0) layout.setVerticalSpacing(0) self.rows = rows self.columns = columns self.tiles = [] for row in range(rows): for column in range(columns): tile = Tile(self) tile.clicked.connect(partial(self.select_tile, tile)) layout.addWidget(tile, row, column) self.tiles.append(tile) def select_tile(self, tile): """Callback when tile is pressed. Send signal with tile as arg""" self.clicked.emit(tile) class TileDialog(QDialog): """Dialog for selecting a tile""" def __init__(self, images): super().__init__() self.setWindowFlags(Qt.FramelessWindowHint) self.tile = None tile_count = len(images) + 1 columns = int(math.ceil(math.sqrt(tile_count))) rows = int(math.ceil(tile_count / columns)) self.tiles = TileControl(self, rows, columns) for tile, image in zip(self.tiles.tiles, images): tile.load_image(image) self.tiles.clicked.connect(self.select_tile) reject_button = QPushButton('Cancel') reject_button.clicked.connect(self.reject) self.layout = QVBoxLayout(self) self.layout.setContentsMargins(2, 2, 2, 2) self.layout.setSpacing(2) self.layout.addWidget(self.tiles) self.layout.addWidget(reject_button) def select_tile(self, tile): """Tile was selected. Save and accept""" self.tile = tile self.accept() class TileWindow(QWidget): """Make a pretty mosaic. Set tile images by selecting image from dialog that pops up when tile is pressed. def""" def __init__(self, rows, columns, images): super().__init__() self.dialog = TileDialog(images) self.tiles = TileControl(self, rows, columns) self.tiles.clicked.connect(self.select_tile) self.layout = QVBoxLayout(self) self.layout.addWidget(self.tiles) def select_tile(self, tile): """A tile was pressed. Get an image from the dialog""" self.dialog.move(QCursor.pos()) if self.dialog.exec_(): tile.load_image(self.dialog.tile.image_file) # Get list of image files in same directory as program PATH = pathlib.Path(sys.path[0]) IMAGES = [PATH/file for file in PATH.iterdir() if file.suffix in ('.png', '.PNG', '.jpg', '.JPG')] APP = QApplication() WINDOW = TileWindow(10, 10, IMAGES) WINDOW.show() sys.exit(APP.exec_())This was a good excuse to learn more about painting in Qt. I'm not sure if I can work up enough interest to stitch all the images together to make a new image.