今日はずいぶんGoogle Geminiさんのお世話になりました。欲しい機能を4つ残して、基本的なアプリが完成しました。
トレース用簡易ペイントのコード
# forTrace.py 2025.09.14
# 手頃なアプリがないので自分で作ることにした。
import sys
from PyQt5.QtCore import Qt, QPointF, QSize
from PyQt5.QtGui import QPainter, QImage, QPen, QColor, QIcon, QBrush, QKeySequence, QPixmap
from PyQt5.QtWidgets import (
QMainWindow, QApplication, QMenu, QMenuBar, QAction, QFileDialog,
QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QSlider, QLabel, QShortcut )
# 色リスト
COLORS = [
# 17 undertones https://lospec.com/palette-list/17undertones
'#000000', '#ffffff', '#35e3e3', '#141923', '#414168', '#3a7fa7', '#8fd970',
'#5ebb49', '#458352', '#dcd37b', '#fffee5', '#ffd035', '#cc9245', '#a15c3e',
'#a42f3b', '#f45b7a', '#c24998', '#81588d', '#bcb0c2'
]
# Canvasクラス
class Canvas(QLabel):
def __init__(self):
super().__init__()
width = 256 * 3
height = 256 * 3
self.setGeometry(0, 0, width, height)
# 描画用のQPixmapを初期化
self.image = QPixmap(width, height)
self.image.fill(QColor("white"))
self.setPixmap(self.image)
# 初期値
self.last_x, self.last_y = None, None
self.pen_color = QColor('#000000')
self.pen_size = 5
def set_pen_color(self, c):
self.pen_color = QColor(c)
def set_pen_size(self, pix):
self.pen_size = pix
def mouseMoveEvent(self, e):
if self.last_x is None: # First event.
self.last_x = e.x()
self.last_y = e.y()
return # Ignore the first time.
painter = QPainter(self.pixmap())
p = painter.pen()
p.setWidth(self.pen_size) # 整数
p.setColor(self.pen_color)
p.setCapStyle(Qt.RoundCap)
p.setJoinStyle(Qt.RoundJoin)
painter.setPen(p)
painter.drawLine(self.last_x, self.last_y, e.x(), e.y())
painter.end()
self.update()
# Update the origin for next time.
self.last_x = e.x()
self.last_y = e.y()
def mouseReleaseEvent(self, e):
self.last_x = None
self.last_y = None
# 外部画像を読み込み、キャンバスに設定するメソッド
def load_image_to_canvas(self, path):
loaded_pixmap = QPixmap(path)
if not loaded_pixmap.isNull():
# 新しいQPixmapを作成し、キャンバスのサイズに合わせる
new_image = QPixmap(self.size())
new_image.fill(QColor("white")) # 背景を白で塗りつぶす
# QPainterを使用して、読み込んだ画像を新しいQPixmapに描画
painter = QPainter(new_image)
# 画像をキャンバスのサイズに合わせてスケール
scaled_pixmap = loaded_pixmap.scaled(self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
# 描画開始位置を計算して中央に配置
x_offset = (new_image.width() - scaled_pixmap.width()) // 2
y_offset = (new_image.height() - scaled_pixmap.height()) // 2
painter.drawPixmap(x_offset, y_offset, scaled_pixmap)
painter.end()
self.image = new_image
self.setPixmap(self.image)
self.update()
return True
return False
# キャンバスをクリアするメソッド
def clear_canvas(self):
# QPixmapオブジェクトを新しく作成して、白で塗りつぶす
self.image = QPixmap(self.size())
self.image.fill(QColor("white"))
self.setPixmap(self.image)
self.update()
# Paletteクラス
class QPaletteButton(QPushButton):
def __init__(self, color):
super().__init__()
self.setFixedSize(QSize(24,24))
self.color = color
self.setStyleSheet("background-color: %s;" % color)
# メインウィンドウ
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.canvas = Canvas()
# パラメータ
title = "forTrace.py"
icon = "icons/face-button.png"
# 描画領域
self.canvas = Canvas()
# タイトルとアイコン
title = "forTrace.py"
icon = "icons/face-button.png"
self.setWindowTitle(title)
self.setWindowIcon(QIcon(icon))
# ウィジェット配置
w = QWidget()
l = QVBoxLayout()
w.setLayout(l)
palette = QHBoxLayout()
self.add_palette_buttons(palette)
l.addLayout(palette)
l.addWidget(self.canvas)
self.setCentralWidget(w)
# メニュー
mainMenu = self.menuBar()
fileMenu = mainMenu.addMenu("ファイル")
brushes = mainMenu.addMenu("ブラシ")
# アクション登録
loadAction = QAction(QIcon("icons/load-button.png"), "読込",self)
loadAction.setShortcut("Ctrl+O")
fileMenu.addAction(loadAction)
loadAction.triggered.connect(self.load)
saveAction = QAction(QIcon("icons/save-button.png"), "保存",self)
saveAction.setShortcut("Ctrl+S")
fileMenu.addAction(saveAction)
saveAction.triggered.connect(self.save)
clearAction = QAction(QIcon("icons/delete-button.png"), "クリア", self)
clearAction.setShortcut("Ctrl+C")
fileMenu.addAction(clearAction)
clearAction.triggered.connect(self.clear)
pix1Action = QAction( QIcon("icons/pixel-1.svg"), "1px", self)
brushes.addAction(pix1Action)
pix1Action.triggered.connect(self.pix1)
pix5Action = QAction( QIcon("icons/pixel-5.svg"), "5px", self)
brushes.addAction(pix5Action)
pix5Action.triggered.connect(self.pix5)
pix10Action = QAction( QIcon("icons/pixel-10.svg"), "10px", self)
brushes.addAction(pix10Action)
pix10Action.triggered.connect(self.pix10)
pix20Action = QAction( QIcon("icons/pixel-20.svg"), "20px", self)
brushes.addAction(pix20Action)
pix20Action.triggered.connect(self.pix20)
fillAction = QAction( QIcon("icons/fill.svg"), "フィル", self)
brushes.addAction(fillAction)
fillAction.triggered.connect(self.fill)
# 読込
def load(self):
# ファイルダイアログを開き、画像ファイルを選択
filePath, _ = QFileDialog.getOpenFileName(self, "Load Image", "", "BMP(*.bmp);;PNG(*.png);;JPEG(*.jpg);;All Files(*.*) ")
if not filePath:
return
if not self.canvas.load_image_to_canvas(filePath):
QMessageBox.warning(self, "エラー", "画像の読み込みに失敗しました。")
# 保存
def save(self):
filePath, _ = QFileDialog.getSaveFileName(self, "Save Image", "", "BMP(*.bmp);;PNG(*.png);;JPEG(*.jpg *.jpeg);;All Files(*.*) ")
if filePath == "":
return
# キャンバスからQPixmapを取得
pixmap = self.canvas.pixmap()
if pixmap.isNull():
# pixmapが存在しない場合は警告を表示して終了
QMessageBox.warning(self, "エラー", "保存する画像がありません。")
return
# QPixmapをQImageに変換して保存
image = pixmap.toImage()
if not image.save(filePath):
QMessageBox.warning(self, "エラー", "画像の保存に失敗しました。")
# クリア
def clear(self):
self.canvas.clear_canvas()
# 線幅1ピクセル(極細)
def pix1(self):
self.canvas.set_pen_size(1)
# 線幅5ピクセル(細)
def pix5(self):
self.canvas.set_pen_size(5)
# 線幅10ピクセル(太)
def pix10(self):
self.canvas.set_pen_size(10)
# 線幅20ピクセル(極太)
def pix20(self):
self.canvas.set_pen_size(20)
# 塗りつぶし
def fill(self):
print('fill 未実装')
# パレット
def add_palette_buttons(self, layout):
for c in COLORS:
b = QPaletteButton(c)
b.pressed.connect(lambda c=c: self.canvas.set_pen_color(c))
layout.addWidget(b)
#.......................................................................#
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
出来上がりはこんな感じ

ファイルメニュー

ブラシメニュー

そしてこれから
あと4つ欲しいのが、
- マウスホィールの上下でズームイン/ズームアウト
- マウスの中ボタンドラッグでパン
- やり直し機能(Ctrl+Z)
- 塗りつぶし
これらの実装ができたら、mtPaintに変えて自作アプリを使います。