#!python # -*- mode: python; Encoding: utf-8; coding: utf-8 -*- # Last updated: <2016/10/19 20:33:54 +0900> """ PySideでQMainWindow上にQGraphicsViewをレイアウトして、マウスで描画してみる カレントフォルダにbrushesフォルダを作成して、 ブラシ画像(png画像、1ブラシ 32x32 dot、1ファイル 256x256 dot)を入れておく。 Request : - Python 2.7 - PySide - Pillow (PIL) 動作確認環境 : Windows10 x64 + Python 2.7.11 + PySide 1.2.4 + Pillow 3.4.2 """ import os import sys import re import math from PySide.QtCore import * from PySide.QtGui import * from PIL import Image, ImageQt width = 800 height = 600 brushesImageName = "default.png" brushes = None col_vari = None rgb_info = None scene_brush = None brushImage = None status = None col_vari_type = 0 def setStatus(msg): """ ステータスバーにメッセージを表示 """ global status if status != None: status.showMessage(msg) def refreshBrush(): """ ブラシ画像を更新 """ global scene_brush if scene_brush != None: scene_brush.updateBrushColor() class BrushImages: """ ブラシ画像ファイル群を保持するクラス。 PILを利用して画像を読み込む """ def __init__(self, imgspath): self.dirName = imgspath self.images = {} # フォルダ内のファイル一覧を取得 files = os.listdir(self.dirName) for s in files: path, ext = os.path.splitext(s) if ext.lower() != ".png": # png以外はスキップ continue # PILのImageとして読み込み fpath = os.path.join(self.dirName, s).replace(os.path.sep, '/') img = Image.open(fpath) self.images[s] = (img, img.mode) def getImageNames(self): """ 画像名一覧をリストで返す """ return self.images.keys() def getImage(self, name): """ PIL Imageを返す """ if not self.images.has_key(name): print "Unknown : %s" % name return None img, mode = self.images[name] return img def getMode(self, name): """ PIL Image のモード文字列を返す """ if not self.images.has_key(name): return "" img, mode = self.images[name] return mode class MyColorInfo(QPushButton): """ 色を表示するWidget。QPushButtonを利用 """ def __init__(self, parent=None): super(MyColorInfo, self).__init__(parent) self.color = QColor(128, 128, 128) self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) # サイズを固定 self.setMinimumSize(48, 48) def paintEvent(self, e): qp = QPainter() qp.begin(self) qp.setPen(QColor(0, 0, 0)) qp.setBrush(self.color) qp.drawRect(2, 2, 44, 44) qp.end() del qp def updateColor(self, rgb): """ 色を更新 """ r, g, b = rgb self.color.setRgb(r, g, b) self.update() class RGBSlider(QWidget): """ RGB値指定スライダー部分をまとめたWidget """ def __init__(self, parent=None): super(RGBSlider, self).__init__(parent) defv = 128 self.rgb = (defv, defv, defv) gb = QGridLayout() # 各Widgetを升目上に並べる gb.setContentsMargins(0, 0, 0, 0) # 外のマージン(隙間)を極力無くす # 色表示部分のWidget self.rgbDispBtn = MyColorInfo(self) gb.addWidget(self.rgbDispBtn, 0, 0, 1, 3) # スライダーやスピンボックス部分のWidget # RGBの3つ分、Slider や SpinBox を確保 self.sld = [] self.spb = [] for (i, s) in enumerate(["R", "G", "B"]): lbl = QLabel(s) sld = QSlider(Qt.Orientation.Horizontal, self) spb = QSpinBox() # レイアウト gb.addWidget(lbl, i + 1, 0) gb.addWidget(sld, i + 1, 1) gb.addWidget(spb, i + 1, 2) # 値の範囲を 0-255 に設定 sld.setRange(0, 255) spb.setRange(0, 255) # 現在値を初期化 sld.setValue(defv) spb.setValue(defv) # Slider や SpinBox 値が変えられた時に呼ばれるメソッドを設定 sld.valueChanged[int].connect(self.sliderChangeValue) spb.valueChanged[int].connect(self.spinBoxChangeValue) # 後で使うので記録しておく self.sld.append(sld) self.spb.append(spb) # 自身のレイアウトとして登録 self.setLayout(gb) def sliderChangeValue(self, value): """ Slider の値が変わった時に呼ばれる処理 """ # SpinBoxの値をSliderの値で更新 for (i, sld) in enumerate(self.sld): self.spb[i].setValue(sld.value()) self.updateColor() def spinBoxChangeValue(self, value): """ SpinBox の値が変わった時に呼ばれる処理 """ # Sliderの値をSpinBoxの値で更新 for (i, spb) in enumerate(self.spb): self.sld[i].setValue(spb.value()) self.updateColor() def updateColor(self): """ 色表示部分を更新 """ v = [] for spb in self.spb: v.append(spb.value()) self.rgb = (v[0], v[1], v[2]) self.rgbDispBtn.updateColor(self.rgb) # 色をブラシ画像にも反映 # 処理が重そうなので、マウスカーソルが領域外に出たタイミングで # 色を反映させたブラシ画像を生成するほうがいいのかもしれない refreshBrush() def getRGB(self): return self.rgb class BrushImageViewScene(QGraphicsScene): """ ブラシ画像を表示するGraphicsScene """ def __init__(self, *argv, **keywords): super(BrushImageViewScene, self).__init__(*argv, **keywords) self.selectBrushIndex = 0 self.buttonFlag = False self.pilImage = None self.pilBrushImage = None self.pixmap = QPixmap(256, 256) self.pixmap.fill() # Scene に Item を追加 self.imgItem = QGraphicsPixmapItem(self.pixmap) self.addItem(self.imgItem) self.selPen = QPen(QColor(0, 0, 0)) self.selRect = self.addRect(0, 0, 31, 31, self.selPen) def setBrushPixmapFromImage(self, img): """ PIL Image を QPixmap に変換して表示 """ self.pilImage = img.copy() imgnew = img.copy().convert("RGBA") qimg = ImageQt.ImageQt(imgnew) pm = QPixmap.fromImage(qimg) self.pixmap = pm self.imgItem.setPixmap(self.pixmap) self.setBrushImage(self.selectBrushIndex) self.update() def setOpenPixmap(self, fpath): self.pixmap = QPixmap(fpath) self.imgItem.setPixmap(self.pixmap) def setBrushSelectRect(self, n): """ ブラシ選択枠の位置を設定 """ y = int(math.floor(n / 8)) * 32 x = int(n % 8) * 32 self.selRect.setRect(x, y, 31, 31) self.update() return x, y def setBrushImage(self, n): """ ブラシ1つ分の画像を切り出して QPixmap に変換 """ x, y = self.setBrushSelectRect(n) imgnew = self.pilImage.copy() img = imgnew.convert("RGBA").crop((x, y, x + 32, y + 32)) self.pilBrushImage = img self.updateBrushColor() def updateBrushColor(self): """ 現在のブラシ色をブラシ画像に反映させる """ # 現在のブラシ色を取得 global rgb_info global brushImage if rgb_info == None: r, g, b = 128, 128, 128 else: r, g, b = rgb_info.getRGB() mode = self.pilImage.mode src = self.pilBrushImage if mode == "L": # ブラシ画像が元グレースケール img = self.makeBrushImageGrayscale(src, r, g, b) elif mode == "P" or mode == "RGBA": # ブラシ画像がパレットモード or RGBA global col_vari_type if col_vari_type == 0: img = self.makeBrushImageWidthColor(src, r, g, b) else: img = self.makeBrushImageWidthColor2(src, r, g, b) else: img = self.pilBrushImage qimg = ImageQt.ImageQt(img) pm = QPixmap.fromImage(qimg) brushImage = pm def makeBrushImageGrayscale(self, srcImg, r, g, b): """ 元グレースケール画像を参照して色を反映したブラシ画像を生成 """ src = srcImg.copy() w, h = src.size dst = Image.new("RGBA", src.size) for y in range(h): for x in range(w): sr, sg, sb, sa = src.getpixel((x, y)) # アルファチャンネルのみ、元画像の明るさ情報を使う dst.putpixel((x, y), (r, g, b, 255 - sr)) return dst def makeBrushImageWidthColor(self, srcImg, r, g, b): """ RGBA画像を参照して色を反映したブラシ画像を生成。 元画像のドットの値が128の時に、指定色がそのまま出るように計算する。 """ src = srcImg.copy() w, h = src.size dst = Image.new("RGBA", src.size) for y in range(h): for x in range(w): sr, sg, sb, sa = src.getpixel((x, y)) sr = self.multiplyValue(sr, r) sg = self.multiplyValue(sg, g) sb = self.multiplyValue(sb, b) dst.putpixel((x, y), (sr, sg, sb, sa)) return dst def multiplyValue(self, v, a): v = int(v * a / 128) if v > 255: v = 255 return v def makeBrushImageWidthColor2(self, srcImg, r, g, b): """ RGBA画像を参照して色を反映したブラシ画像を生成。 元画像のドット値を上下にずらす。 """ src = srcImg.copy() w, h = src.size dst = Image.new("RGBA", src.size) for y in range(h): for x in range(w): sr, sg, sb, sa = src.getpixel((x, y)) sr = self.shiftValue(sr, r) sg = self.shiftValue(sg, g) sb = self.shiftValue(sb, b) dst.putpixel((x, y), (sr, sg, sb, sa)) return dst def shiftValue(self, v, a): v = int(v + a - 128) if v < 0: v = 0 if v > 255: v = 255 return v def mouseDoubleClickEvent(self, event): pass def mousePressEvent(self, event): """ マウスボタンが押された際の処理 """ if event.button() == Qt.LeftButton: if not self.buttonFlag: self.buttonFlag = True # ブラシ選択 x = int(math.floor(event.scenePos().x() / 32)) y = int(math.floor(event.scenePos().y() / 32)) self.selectBrushIndex = y * 8 + x self.setBrushImage(self.selectBrushIndex) elif event.button() == Qt.RightButton: pass def mouseMoveEvent(self, event): """ マウスカーソル移動中の処理 """ pass def mouseReleaseEvent(self, event): """ マウスボタンが離されたときの処理 """ if event.button() == Qt.LeftButton: self.buttonFlag = False class MyBrushWidget(QWidget): """ メインウインドウ左側に配置するウィジェット """ def __init__(self, parent=None): super(MyBrushWidget, self).__init__(parent) self.myLayout = QVBoxLayout() # 縦に並べる self.addBrushImageSelect() # ブラシ画像選択部分 self.addBrushImageView() # ブラシ画像表示部分 self.addColrVariation() # 色反映処理選択部分 self.addColorView() # 色選択部分 self.addSpacer() # スペーサー self.setLayout(self.myLayout) # 自身のレイアウトとして設定 def addBrushImageSelect(self): """ ブラシ画像選択用 ComboBox をレイアウトに登録 """ self.cb = QComboBox(self) global brushes for s in brushes.getImageNames(): self.cb.addItem(s) self.myLayout.addWidget(self.cb) # ComboBoxが変更されたときに呼ばれる処理を設定 self.cb.currentIndexChanged.connect(self.currentBrushImageChanged) def addColrVariation(self): """ 色反映処理選択 ComboBox をレイアウトに登録 """ global col_vari col_vari = QComboBox(self) col_vari.addItem("Color Chnage : Type A") col_vari.addItem("Color Change : Type B") self.myLayout.addWidget(col_vari) col_vari.currentIndexChanged.connect(self.currentColorVariChanged) def addBrushImageView(self): """ ブラシ画像表示用 GraphicsView をレイアウトに登録 """ global brushesImageName global scene_brush scene_brush = BrushImageViewScene() self.showBrushImage(brushesImageName) self.gview_brush = QGraphicsView(scene_brush, self) # Viewサイズを固定 self.gview_brush.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.gview_brush.setMinimumSize(256, 256) # スクロールバー非表示 self.gview_brush.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.gview_brush.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.myLayout.addWidget(self.gview_brush) def addColorView(self): """ 色選択用 Widget をレイアウトに登録 """ global rgb_info rgb_info = RGBSlider(self) self.myLayout.addWidget(rgb_info) def addSpacer(self): """ スペーサーをレイアウトに登録 """ spc = QSpacerItem(16, 16, QSizePolicy.Expanding, QSizePolicy.Expanding) self.myLayout.addSpacerItem(spc) def currentBrushImageChanged(self, n): """ ブラシ画像を変更した時に呼ばれる処理 """ global brushes global brushesImageName brushesImageName = brushes.getImageNames()[n] self.showBrushImage(brushesImageName) # ステータスバーにブラシ画像のモードを表示 mode = brushes.getMode(brushesImageName) if mode == "L": mode = "Grayscale" elif mode == "P" or mode == "RGBA": mode = "RGBA" else: mode = "Unknown" setStatus("Brush Mode : %s" % mode) def showBrushImage(self, name): """ ブラシ画像を表示するように設定 """ global brushes global scene_brush img = brushes.getImage(name) scene_brush.setBrushPixmapFromImage(img) # scene_brush.setOpenPixmap("./brushes/default.png") def currentColorVariChanged(self, n): """ 色反映処理が変更されたときに呼ばれる処理 """ global col_vari_type col_vari_type = n refreshBrush() class DrawAreaScene(QGraphicsScene): """ 描画ウインドウ用Scene """ def __init__(self, *argv, **keywords): super(DrawAreaScene, self).__init__(*argv, **keywords) self.buttonFlag = False # ブラシを描き込むための、中身が空のQPixmapを用意 self.pixmap = QPixmap(width, height) self.pixmap.fill(QColor(0, 0, 0, 0)) # 背景のチェックボード柄を用意 bg = QPixmap(width, height) bg.fill(QColor(0, 0, 0, 0)) # Scene に Item を追加 self.chkbrdItem = QGraphicsPixmapItem(bg) self.addItem(self.chkbrdItem) self.imgItem = QGraphicsPixmapItem(self.pixmap) self.addItem(self.imgItem) # 背景にチェックボード柄を設定 self.chkbrdBrush = QBrush(self.makeCheckBoardPixmap()) self.setChkbrdBg(self.pixmap.width(), self.pixmap.height()) def setChkbrdBg(self, w, h): """ 背景のチェックボード柄画像を更新 """ bg = QPixmap(w, h) qp = QPainter() qp.begin(bg) qp.fillRect(0, 0, w, h, self.chkbrdBrush) qp.end() del qp self.chkbrdItem.setPixmap(bg) def makeCheckBoardPixmap(self, bgcol=255, graycol=204): """ チェックボード柄のQPixmapを生成して返す """ bg = QPixmap(16, 16) qp = QPainter() qp.begin(bg) qp.fillRect(0, 0, 16, 16, QColor(bgcol, bgcol, bgcol)) col = QColor(graycol, graycol, graycol) qp.fillRect(0, 0, 8, 8, col) qp.fillRect(8, 8, 8, 8, col) qp.end() del qp return bg def mouseDoubleClickEvent(self, event): pass def mousePressEvent(self, event): """ マウスボタンが押された際の処理 """ if event.button() == Qt.LeftButton: # ブラシ描画開始 self.buttonFlag = True self.drawing(event) elif event.button() == Qt.RightButton: # 右クリックで画面クリア self.pixmap.fill(QColor(0, 0, 0, 0)) self.imgItem.setPixmap(self.pixmap) def mouseMoveEvent(self, event): """ マウスカーソル移動中の処理 """ if self.buttonFlag: self.drawing(event) def mouseReleaseEvent(self, event): """ マウスボタンが離されたときの処理 """ if event.button() == Qt.LeftButton: self.buttonFlag = False def drawing(self, event): """ ブラシで描画 """ global brushImage bimg = brushImage x = int(event.scenePos().x()) y = int(event.scenePos().y()) x -= int(math.floor(bimg.width() / 2)) y -= int(math.floor(bimg.height() / 2)) qp = QPainter() qp.begin(self.pixmap) qp.drawPixmap(x, y, bimg) qp.end() del qp self.imgItem.setPixmap(self.pixmap) class DrawAreaView(QGraphicsView): """ メインとなるビュー """ def __init__(self, *argv, **keywords): super(DrawAreaView, self).__init__(*argv, **keywords) self.setCacheMode(QGraphicsView.CacheBackground) # self.setRenderHints(QPainter.Antialiasing | # QPainter.SmoothPixmapTransform | # QPainter.TextAntialiasing # ) # 背景色を設定 self.setBackgroundBrush(Qt.darkGray) # sceneを登録 scene = DrawAreaScene(self) self.setScene(scene) scene.setSceneRect(QRectF(self.rect())) def resizeEvent(self, event): """ ビューをリサイズ時にシーンの矩形を更新 """ super(DrawAreaView, self).resizeEvent(event) self.scene().setSceneRect(QRectF(self.rect())) class MyMainWindow(QMainWindow): """ メインウインドウ """ def __init__(self, parent=None): super(MyMainWindow, self).__init__(parent) # メニューバー mb = QMenuBar() file_menu = QMenu("&File", self) exit_action = file_menu.addAction("&Close") exit_action.setShortcut('Ctrl+Q') exit_action.triggered.connect(qApp.quit) mb.addMenu(file_menu) self.setMenuBar(mb) # ステータスバー global status status = QStatusBar(self) status.showMessage("Status Bar") self.setStatusBar(status) # 左側のドックウィジェット self.leftDock = QDockWidget("Brush", self) self.brushw = MyBrushWidget(self) self.leftDock.setWidget(self.brushw) # 移動とフローティングは有効にするが閉じるボタンは無効にする # 注意: メインウインドウが十分大きくないと、 # フローティング後、元に戻せなくなる self.leftDock.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea) self.leftDock.setFeatures(QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetFloatable) self.addDockWidget(Qt.LeftDockWidgetArea, self.leftDock) # 中央ウィジェット self.gview_image = DrawAreaView(self) self.setCentralWidget(self.gview_image) def main(): """ メイン """ # ブラシ画像読み込み dpath = os.path.join(os.getcwdu(), "brushes") global brushes brushes = BrushImages(dpath) # メインウインドウ生成 app = QApplication(sys.argv) # app.setStyle(QStyleFactory.create('Cleanlooks')) w = MyMainWindow() w.setWindowTitle("Draw Brush Test") w.resize(800, 600) w.show() sys.exit(app.exec_()) if __name__ == '__main__': main()