フレームを挿入する
リスト再描画バグも修正した。
成果物
format | image | animation |
---|---|---|
gif |
||
png |
||
webp |
||
txt |
pixels.txt | - |
コード
コード(670行)
main.py
#!/usr/bin/python3 # -*- coding: utf-8 -*- import sys, os, numpy, PIL from PySide2 import QtCore, QtGui, QtWidgets from PIL import Image, ImagePalette class Window(QtWidgets.QMainWindow): def __init__(self): super(self.__class__, self).__init__() self.setAcceptDrops(True) self.widget = Widget(self) self.setCentralWidget(self.widget) menu_file = QtWidgets.QMenu('File', self) menu_file.addAction(self.widget.GraphicsView.Scene.Drawable.SaveAction) self.menuBar().addMenu(menu_file) menu_frame = QtWidgets.QMenu('Animation', self) menu_frame.addAction(FrameListView.AddFrameAction) menu_frame.addAction(FrameListView.InsertCopyFrameAction) menu_frame.addAction(FrameListView.DeleteFrameAction) self.menuBar().addMenu(menu_frame) self.show() # Frame側でも使いたいので globals()['Window'] = self def mousePressEvent(self, event): super(self.__class__, self).mousePressEvent(event) self.widget.update() def mouseMoveEvent(self, event): super(self.__class__, self).mouseMoveEvent(event) self.widget.update() def mouseReleaseEvent(self, event): super(self.__class__, self).mouseReleaseEvent(event) self.widget.update() def dragEnterEvent(self, event): super(self.__class__, self).dragEnterEvent(event) self.widget.update() def dragMoveEvent(self, event): super(self.__class__, self).dragMoveEvent(event) def dropEvent(self, event): super(self.__class__, self).dropEvent(event) class Widget(QtWidgets.QWidget): def __init__(self, parent): super(self.__class__, self).__init__(parent) self.setAcceptDrops(True) self.view = GraphicView() self.animation = AnimationWidget() # self.animation.setMinimumHeight(self.animation.height()) # self.animation.setMaximumHeight(self.animation.height()*1.2) self.animation.setMinimumHeight(32) self.animation.setMaximumHeight(64) self.animation.resize(self.animation.width(), self.animation.height()) globals()['AnimationWidget'] = self.animation scroller1 = QtWidgets.QScrollArea() scroller1.setWidget(self.view) layout = QtWidgets.QGridLayout() layout.addWidget(scroller1, 0, 0) layout.addWidget(self.animation, 1, 0) self.setLayout(layout) self.resize(self.view.width(), self.view.height()) self.setWindowTitle("QAction") self.show() @property def GraphicsView(self): return self.view def mousePressEvent(self, event): super(self.__class__, self).mousePressEvent(event) self.view.scene().update() self.view.update() # self.animation.mousePressEvent(event) # self.animation.update() def mouseMoveEvent(self, event): super(self.__class__, self).mouseMoveEvent(event) self.view.scene().update() self.view.update() # self.animation.mouseMoveEvent(event) # self.animation.update() def mouseReleaseEvent(self, event): super(self.__class__, self).mouseReleaseEvent(event) self.view.scene().update() self.view.update() # self.animation.mouseReleaseEvent(event) # self.animation.update() def dragEnterEvent(self, event): super(self.__class__, self).dragEnterEvent(event) self.view.dragEnterEvent(event) self.view.scene().update() self.view.update() # self.animation.dragEnterEvent(event) # self.animation.update() def dragMoveEvent(self, event): super(self.__class__, self).dragMoveEvent(event) self.view.scene().update() self.view.update() # self.animation.dragMoveEvent(event) # self.animation.update() def dropEvent(self, event): super(self.__class__, self).dropEvent(event) self.view.scene().update() self.view.update() # self.animation.dropEvent(event) # self.animation.update() class GraphicView(QtWidgets.QGraphicsView): def __init__(self): QtWidgets.QGraphicsView.__init__(self) self.setAcceptDrops(True) self.setWindowTitle("QGraphicsScene draw Grid") self.__editorScene = EditorScene(self) self.setScene(self.__editorScene) def mousePressEvent(self, event): super(self.__class__, self).mousePressEvent(event) self.scene().update() def mouseMoveEvent(self, event): super(self.__class__, self).mouseMoveEvent(event) self.scene().update() def mouseReleaseEvent(self, event): super(self.__class__, self).mouseReleaseEvent(event) self.scene().update() @property def Scene(self): return self.__editorScene def dragEnterEvent(self, event): super(self.__class__, self).dragEnterEvent(event) self.scene().update() def dragEnterEvent(self, event): super(self.__class__, self).dragMoveEvent(event) self.scene().update() def dropEvent(self, event): super(self.__class__, self).dropEvent(event) self.scene().update() class EditorScene(QtWidgets.QGraphicsScene): def __init__(self, *args, **kwargs): super(self.__class__, self).__init__(*args, **kwargs) self.size = 16 self.scale = 32 self.setSceneRect(0, 0, self.size*self.scale, self.size*self.scale) self.grid = GridItem() self.addItem(self.grid) self.background = BackgroundItem() self.addItem(self.background) self.drawable = DrawableItem() self.addItem(self.drawable) self.background.setZValue(0) self.drawable.setZValue(1) self.grid.setZValue(9999) # Frame側でも使いたいので globals()['Drawable'] = self.drawable def mousePressEvent(self, event): for item in self.items(): item.mousePressEvent(event) super(self.__class__, self).mousePressEvent(event) def mouseMoveEvent(self, event): for item in self.items(): item.setAcceptHoverEvents(True) item.mouseMoveEvent(event) super(self.__class__, self).mouseMoveEvent(event) def mouseReleaseEvent(self, event): for item in self.items(): item.mouseReleaseEvent(event) super(self.__class__, self).mousePressEvent(event) def dragEnterEvent(self, event): for item in self.items(): item.setAcceptDrops(True) if event is type(QtWidgets.QGraphicsSceneDragDropEvent): item.dragEnterEvent(event) if event is type(QtWidgets.QGraphicsSceneDragDropEvent): super(self.__class__, self).dragEnterEvent(event) def dragMoveEvent(self, event): for item in self.items(): item.setAcceptDrops(True) if event is type(QtWidgets.QGraphicsSceneDragDropEvent): item.dragEnterEvent(event) if event is type(QtWidgets.QGraphicsSceneDragDropEvent): super(self.__class__, self).dragEnterEvent(event) def dropEvent(self, event): for item in self.items(): item.setAcceptDrops(True) item.dropEvent(event) if event is type(QtWidgets.QGraphicsSceneDragDropEvent): super(self.__class__, self).dropEvent(event) @property def Grid(self): return self.grid @property def Background(self): return self.background @property def Drawable(self): return self.drawable class DrawableItem(QtWidgets.QGraphicsRectItem): def __init__(self, *args, **kwargs): super(self.__class__, self).__init__(*args, **kwargs) self.setAcceptDrops(True) self.setAcceptHoverEvents(True) self.scale = 32 self.pixels = Pixels() self.actions = {} self.__create_save_action() img = QtGui.QImage(self.pixels.Width, self.pixels.Height, QtGui.QImage.Format_ARGB32) img.fill(QtGui.QColor(0,0,0,0)) # self.pixmap = QtGui.QPixmap(self.pixels.Width, self.pixels.Height) self.pixmap = QtGui.QPixmap.fromImage(img) print('Alpha:', self.pixmap.hasAlpha()) self.__draw_pos = [] self.freehand = FreeHand() def __create_save_action(self): a = QtWidgets.QAction('Save') a.setObjectName('Save') a.setShortcut('Ctrl+S') a.triggered.connect(self.Pixels.save) self.actions['Save'] = a def paint(self, painter, option, widget): painter.drawPixmap(0, 0, self.pixels.Width*self.scale, self.pixels.Height*self.scale, self.pixmap) def mouseMoveEvent(self, event): pos = event.scenePos() x = int(pos.x()//self.scale) y = int(pos.y()//self.scale) x = max(0, x) y = max(0, y) x = min(x, self.pixels.Width-1) y = min(y, self.pixels.Height-1) if event.buttons() & QtCore.Qt.LeftButton: self.freehand.Color = QtGui.QColor(255,0,0) self.freehand.draw(self.pixmap, x, y) self.__update_frame_list(event) if event.buttons() & QtCore.Qt.RightButton: self.freehand.Color = QtGui.QColor(0,0,0,0) self.freehand.draw(self.pixmap, x, y) self.__update_frame_list(event) def mousePressEvent(self, event): pos = event.scenePos() x = int(pos.x()//self.scale) y = int(pos.y()//self.scale) x = max(0, x) y = max(0, y) x = min(x, self.pixels.Width-1) y = min(y, self.pixels.Height-1) if event.buttons() & QtCore.Qt.LeftButton: self.freehand.Color = QtGui.QColor(255,0,0) self.freehand.draw(self.pixmap, x, y) self.__update_frame_list(event) if event.buttons() & QtCore.Qt.RightButton: self.freehand.Color = QtGui.QColor(0,0,0,0) self.freehand.draw(self.pixmap, x, y) self.__update_frame_list(event) def mouseReleaseEvent(self, event): pos = event.scenePos() x = int(pos.x()//self.scale) y = int(pos.y()//self.scale) x = max(0, x) y = max(0, y) x = min(x, self.pixels.Width-1) y = min(y, self.pixels.Height-1) self.freehand.draw(self.pixmap, x, y) self.freehand.points.clear() self.__update_frame_list(event) def __update_frame_list(self, event): for idx in FrameListView.selectedIndexes(): # 再描画。なぜかマウスイベントを発火すると再描画される。なぜかリストの先頭項目が選択されるため後でsetCurrentIndexすべし。 FrameListView.mouseMoveEvent(QtGui.QMouseEvent(QtCore.QEvent.MouseMove, event.pos(), event.button(), event.buttons(), QtCore.Qt.NoModifier)) FrameListView.Model.update_pixmap(idx, self.pixmap) FrameListView.setCurrentIndex(FrameListView.Model.index(idx.row(),0)) def mouseDoubleClickEvent(self, event): pass @property def Pixels(self): return self.pixels @Pixels.setter def Pixels(self, value): for y in range(value.Height): for x in range(value.Width): self.pixels.Pixels[y][x] = value.Pixels[y][x] @property def SaveAction(self): return self.actions['Save'] def dragEnterEvent(self, event): if event.mimeData().hasUrls(): event.acceptProposedAction() def dropEvent(self, event): for url in event.mimeData().urls(): file_name = url.toLocalFile() print("Dropped file: " + file_name) self.Pixels.load(file_name) class FreeHand: def __init__(self, *args, **kwargs): self.points = [] self.color = QtGui.QColor(255,0,0,0) @property def Color(self): return self.color @Color.setter def Color(self, value): if isinstance(value, QtGui.QColor): self.color = value def draw(self, pixmap, x, y): self.points.append((x,y)) painter = QtGui.QPainter(pixmap) # painter.setBrush(QtGui.QBrush(QtGui.QColor(255,0,0,128), QtCore.Qt.SolidPattern)) painter.setBrush(self.color) painter.setPen(self.color) painter.setCompositionMode(QtGui.QPainter.CompositionMode_Source) # painter.fillRect(pixmap.rect(), QtCore.Qt.transparent); # painter.setCompositionMode(QtGui.QPainter.CompositionMode_SourceOver); if 1 == len(self.points): painter.drawLine(self.points[0][0], self.points[0][1], self.points[0][0], self.points[0][1]) elif 1 < len(self.points): for i in range(len(self.points)-1): painter.drawLine(self.points[i][0], self.points[i][1], self.points[i+1][0], self.points[i+1][1]) print(self.points[i][0], self.points[i][1], self.points[i+1][0], self.points[i+1][1]) painter.end() class BackgroundItem(QtWidgets.QGraphicsRectItem): def __init__(self, *args, **kwargs): super(self.__class__, self).__init__(*args, **kwargs) self.size = 16 self.scale = 32 self.colors = [QtGui.QColor(196,196,196,255), QtGui.QColor(232,232,232,255)] def paint(self, painter, option, widget): for i in range(self.size*self.size): x = (i % self.size) y = (i // self.size) color = QtGui.QColor(128,128,128,255) if 0 == (i % 2) and 0 == (x % 2) else QtGui.QColor(196,196,196,255) painter.fillRect(x * (self.scale), y * (self.scale), self.scale//2, self.scale//2, self.colors[0]) painter.fillRect(x * (self.scale)+self.scale//2, y * (self.scale)+self.scale//2, self.scale//2, self.scale//2, self.colors[0]) painter.fillRect(x * (self.scale)+self.scale//2, y * (self.scale), self.scale//2, self.scale//2, self.colors[1]) painter.fillRect(x * (self.scale), y * (self.scale)+self.scale//2, self.scale//2, self.scale//2, self.colors[1]) class GridItem(QtWidgets.QGraphicsRectItem): def __init__(self, *args, **kwargs): super(self.__class__, self).__init__(*args, **kwargs) self.size = 16 self.scale = 32 def paint(self, painter, option, widget): painter.fillRect(widget.rect(), QtGui.QBrush(QtGui.QColor(0,0,0,0), QtCore.Qt.SolidPattern)) lines = [] for y in range(self.size+1): lines.append(QtCore.QLine(0, y*self.scale, self.size*self.scale, y*self.scale)) for x in range(self.size+1): lines.append(QtCore.QLine(x*self.scale, 0, x*self.scale, self.size*self.scale)) painter.drawLines(lines) class Pixels: def __init__(self): self.width = 16 self.height = 16 self.pixels = numpy.zeros(self.width*self.height, dtype=int).reshape(self.height, self.width) @property def Pixels(self): return self.pixels @property def Width(self): return self.width @property def Height(self): return self.height def save(self): print(os.getcwd()) self.save_txt() for ext in ('gif', 'png', 'webp'): self.save_raster(ext) for ext in ('gif', 'png', 'webp'): self.save_animation(ext) def load(self, file_path): ext = os.path.splitext(file_path)[1].lower()[1:] if '' == ext: raise Exception('拡張子が必要です。png,gif,webp,txt形式のいずれかに対応しています。') elif 'txt' == ext: self.load_txt(file_path) elif 'gif' == ext: self.load_gif(file_path) elif 'png' == ext: self.load_png(file_path) elif 'webp' == ext: self.load_webp(file_path) else: raise Exception('拡張子が未対応です。png,gif,webp,txt形式のいずれかに対応しています。') def save_txt(self): with open(os.path.join(os.getcwd(), 'pixels.txt'), 'w') as f: f.write('\n'.join([''.join(map(str, self.pixels[y].tolist())) for y in range(self.height)])) def load_txt(self, file_path): with open(file_path, 'r') as f: lines = f.read().split('\n') self.height = len(lines) self.width = len(lines[0]) self.pixels = numpy.zeros(self.width*self.height, dtype=int).reshape(self.height, self.width) x = 0; y = 0; for line in lines: for c in line: self.pixels[y][x] = int(c, 16) x += 1 y += 1 x = 0 def save_raster(self, ext): for index in FrameListView.selectedIndexes(): qimg = FrameListView.Model.Frames[index.row()].pixmap.toImage() image = Image.new('1', (self.width, self.height)) image.putdata([0 if 0 == qimg.pixel(x,y) else 1 for y in range(qimg.width()) for x in range(qimg.height())]) print(ext) image.save(os.path.join(os.getcwd(), 'pixels.' + ext), optimize=True, lossless=True, disposal=2, transparency=0) def save_animation(self, ext): print(ext) if len(FrameListView.Model.Frames) < 2: return images = [] for frame in FrameListView.Model.Frames: image = Image.new('P', (frame.Pixels.Width, frame.Pixels.Height)) image.putpalette([0,0,0,255,255,255]) qimg = frame.pixmap.toImage() image.putdata([0 if 0 == qimg.pixel(x,y) else 1 for y in range(frame.pixmap.width()) for x in range(frame.pixmap.height())]) images.append(image) p = {} p['save_all'] = True p['append_images'] = images p['duration'] = AnimationDurationSetDialog.Duration p['loop'] = AnimationDurationSetDialog.Loop p['optimize'] = False p['transparency'] = 0 if 'gif' == ext: p['disposal'] = 2 image.save(os.path.join(os.getcwd(), 'animation.' + ext), **p) def load_png(self, file_path): image = Image.open(file_path, mode='r') image = image.convert('1') print(len(image.getdata()), list(image.getdata())) self.pixels = numpy.array(list(map(lambda x: 0 if 0 == x else 1, list(image.getdata())))).reshape(image.size[1], image.size[0]) self.width, self.height = image.size print(self.width, self.height) def load_gif(self, file_path): # 値が0/255で出力されてしまうので0/1に変換する image = Image.open(file_path, mode='r') self.width, self.height = image.size self.pixels = numpy.array(list(map(lambda x: 0 if 0 == x else 1, list(image.getdata())))).reshape(self.height, self.width) def load_webp(self, file_path): # 値が[0,0,0]/[255,255,255]で出力されてしまうので0/1に変換する image = Image.open(file_path, mode='r') self.width, self.height = image.size self.pixels = numpy.array(list(map(lambda x: 0 if (0,0,0) == x else 1, list(image.getdata())))).reshape(self.height, self.width) class AnimationWidget(QtWidgets.QWidget): def __init__(self, parent=None): super(self.__class__, self).__init__(parent) self.frame_list = FrameListView() globals()['FrameListView'] = self.frame_list self.label = AnimationLabel() globals()['AnimationLabel'] = self.label layout = QtWidgets.QBoxLayout(QtWidgets.QBoxLayout.LeftToRight) layout.addWidget(self.label) layout.addWidget(self.frame_list) self.setLayout(layout) @property def Label(self): return self.label @property def FrameListView(self): return self.frame_list class AnimationLabel(QtWidgets.QLabel): def __init__(self, parent=None): super(self.__class__, self).__init__(parent) self.__is_stop = True self.__frame_index = 0 self.start_animation() def mousePressEvent(self, event): if event.buttons() & QtCore.Qt.LeftButton: self.__is_stop = not self.__is_stop print('is_stop:', self.__is_stop) self.start_animation() def start_animation(self): self.setPixmap(FrameListView.Model.Frames[self.__frame_index].Icon.pixmap(16,16)) if not self.__is_stop: if self.__frame_index < FrameListView.Model.rowCount()-1: self.__frame_index += 1 else: self.__frame_index = 0 QtCore.QTimer.singleShot(AnimationDurationSetDialog.Duration, self.start_animation) class FrameListView(QtWidgets.QListView): def __init__(self, parent=None): super(self.__class__, self).__init__(parent) self.resizeContents(16*16, 16) self.model = FrameListModel() self.model.appendRow() self.setModel(self.model) globals()['FrameListModel'] = self.model self.resize(16*32, 32) self.actions = {} self.__create_add_frame_action() self.__create_delete_frame_action() self.__create_insert_copy_frame_action() self.setCurrentIndex(self.model.index(0,0)) self.setFlow(QtWidgets.QListView.LeftToRight) self.duration_dialog = AnimationDurationSetDialog() globals()['AnimationDurationSetDialog'] = self.duration_dialog self.show() def mouseMoveEvent(self, event): super(self.__class__, self).mousePressEvent(event) def mousePressEvent(self, event): super(self.__class__, self).mousePressEvent(event) for idx in self.selectedIndexes(): frame = idx.data(QtCore.Qt.UserRole) Drawable.Pixels = frame.Pixels Drawable.pixmap = frame.pixmap Window.widget.view.scene().update() if event.buttons() & QtCore.Qt.RightButton: self.duration_dialog.show() def update_pixmap(self, pixmap): for idx in self.selectedIndexes(): self.model.update_icon(idx, pixmap) @property def Model(self): return self.model @property def AddFrameAction(self): return self.actions['AddFrame'] @property def InsertCopyFrameAction(self): return self.actions['InsertCopyFrame'] @property def DeleteFrameAction(self): return self.actions['DeleteFrame'] def __create_add_frame_action(self): a = QtWidgets.QAction('Add frame') a.setObjectName('AddFrame') # a.setShortcut('Ctrl+Alt+N') a.setShortcut('Ins') a.triggered.connect(self.__add_frame) self.actions['AddFrame'] = a def __create_insert_copy_frame_action(self): a = QtWidgets.QAction('Insert copy frame') a.setObjectName('InsertCopyFrame') # a.setShortcut('Alt+C') a.setShortcut('Alt+Ins') a.triggered.connect(self.__insert_copy_frame) self.actions['InsertCopyFrame'] = a def __create_delete_frame_action(self): a = QtWidgets.QAction('Delete frame') a.setObjectName('DeleteFrame') # a.setShortcut('Ctrl+Alt+D') a.setShortcut('Del') a.triggered.connect(self.__delete_frame) self.actions['DeleteFrame'] = a def __add_frame(self): self.model.appendRow() self.setCurrentIndex(self.model.index(len(self.model.Frames)-1,0)) self.__show_drawable() def __insert_new_frame(self): idx = self.selectedIndexes() self.model.insertRow(idx.row()) self.__show_drawable() def __insert_copy_frame(self): idx = self.selectedIndexes() self.model.insertRow(idx[0].row(), self.model.Frames[idx[0].row()].pixmap.copy()) self.__show_drawable() def __delete_frame(self): if len(self.model.Frames) < 2: return for idx in self.selectedIndexes(): if idx.row() == self.model.rowCount()-1: self.setCurrentIndex(self.model.index(idx.row()-1,0)) else: self.setCurrentIndex(self.model.index(idx.row(),0)) self.model.removeRow(idx) self.__show_drawable() def __show_drawable(self): for idx in self.selectedIndexes(): frame = idx.data(QtCore.Qt.UserRole) # Drawable.Pixels = frame.Pixels Drawable.pixmap = frame.pixmap.copy() Window.widget.view.scene().update() class FrameListModel(QtCore.QAbstractListModel): def __init__(self, parent=None): super(self.__class__, self).__init__(parent) self.frames = [] def rowCount(self, parent=QtCore.QModelIndex()): if parent.isValid(): return 0 return len(self.frames) def data(self, index, role=QtCore.Qt.DisplayRole): if role == QtCore.Qt.DecorationRole: return self.frames[index.row()].Icon elif role == QtCore.Qt.UserRole: return self.frames[index.row()] def appendRow(self, pixmap=None): self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount()) self.frames.append(Frame(pixmap)) self.endInsertRows() def insertRow(self, index, pixmap=None): self.beginInsertRows(QtCore.QModelIndex(), index, index) # self.frames.append(Frame(pixmap)) self.frames.insert(index, Frame(pixmap)) self.endInsertRows() def removeRow(self, index): self.beginRemoveRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount()) print(index.row()) self.frames.pop(index.row()) self.endRemoveRows() def update_pixmap(self, index, pixmap): self.frames[index.row()].update_pixmap(pixmap) @property def Frames(self): return self.frames class Frame: def __init__(self, pixmap=None): self.pixels = Pixels() if pixmap: self.pixmap = pixmap else: img = QtGui.QImage(self.pixels.Width, self.pixels.Height, QtGui.QImage.Format_ARGB32) img.fill(QtGui.QColor(0,0,0,0)) self.pixmap = QtGui.QPixmap.fromImage(img) self.icon = QtGui.QImage(self.pixels.Width, self.pixels.Height, QtGui.QImage.Format_Mono) self.update_pixmap(self.pixmap) def __init_pixmap(self): painter = QtGui.QPainter(self.pixmap) painter.setBrush(QtGui.QBrush(QtGui.QColor(0,0,0), QtCore.Qt.SolidPattern)) painter.fillRect(self.pixmap.rect(), QtGui.QColor(0,0,0)) painter.end() def update_pixmap(self, pixmap): self.pixmap = pixmap self.icon = QtGui.QIcon(pixmap) @property def Pixels(self): return self.pixels @Pixels.setter def Pixels(self, value): self.pixels = value @property def Icon(self): return self.icon @Icon.setter def Icon(self, value): self.icon = value class AnimationDurationSetDialog(QtWidgets.QDialog): def __init__(self, parent=None): super(self.__class__, self).__init__(parent) self.setWindowTitle("Set duration") self.duration = QtWidgets.QSpinBox() self.loop = QtWidgets.QSpinBox() self.duration.setMinimum(0) self.duration.setMaximum(1000*60*60*24) self.duration.setSingleStep(1) self.loop.setMinimum(0) self.loop.setMaximum(2**30) self.loop.setSingleStep(1) self.duration.setValue(100) self.loop.setValue(0) layout = QtWidgets.QFormLayout() layout.addRow("Duration", self.duration) layout.addRow("Loop time", self.loop) self.setLayout(layout) self.x = self.geometry().x() self.y = self.geometry().y() self.w = 0 self.h = 0 def show(self): self.setGeometry(self.x, self.y, self.w, self.h) super(self.__class__, self).show() def keyPressEvent(self, event): if event.key() == QtCore.Qt.Key_Escape: self.x = self.geometry().x() self.y = self.geometry().y() self.w = self.geometry().width() self.h = self.geometry().height() super(self.__class__, self).keyPressEvent(event) @property def Duration(self): return self.duration.value() @property def Loop(self): return self.loop.value() if __name__ == "__main__": app = QtWidgets.QApplication(sys.argv) window = Window() sys.exit(app.exec_())
python3 main.py
苦労した所
リスト再描画バグ改修
ドット絵を描画したとき、リアルタイムでリストのアイコンも再描画したい。最初、update()
,repaint()
で行ったが再描画されず。なぜかマウスクリックすると再描画されることを発見。そこで何の処理もしないmouseMoveEvent()
を発火させることで再描画させることにした。ところが、なぜかリスト項目の先頭が選択される別のバグが発生。ここまでが前回。
今回はマウスイベント発火後にListView.setCurrentIndex()
することで選択中のリスト項目を再度選択させることで上記バグを回避した。
def __update_frame_list(self, event): for idx in FrameListView.selectedIndexes(): # 再描画。なぜかマウスイベントを発火すると再描画される。なぜかリストの先頭項目が選択されるため後でsetCurrentIndexすべし。 FrameListView.mouseMoveEvent(QtGui.QMouseEvent(QtCore.QEvent.MouseMove, event.pos(), event.button(), event.buttons(), QtCore.Qt.NoModifier)) FrameListView.Model.update_pixmap(idx, self.pixmap) FrameListView.setCurrentIndex(FrameListView.Model.index(idx.row(),0))
フレーム追加
新規フレーム追加だけでなくフレームのコピー追加もできるようにした。
キー | 意味 | 動機 |
---|---|---|
Ins |
透明な新規フレームを末尾に追加する | どのフレームとも類似していない |
Alt +Ins |
選択しているフレームのコピーを直後に追加する | 直前のフレームと類似している |
マトリクスにすると以下。
追加する | 直後 | 末尾 |
---|---|---|
透明 | - | Ins |
コピー | Alt +Ins |
- |
不足している組合せとその使用動機は以下。
キー | 意味 | 動機 |
---|---|---|
- | 透明な新規フレームを直後に追加する | 直前フレームとは類似していない |
- | 選択しているフレームのコピーを末尾に追加する | 離れたフレームだが類似している |
これについては、いずれ追加予定の「フレーム移動」と組み合わせることで解消するつもり。
class FrameListView(QtWidgets.QListView): ... def __create_add_frame_action(self): a = QtWidgets.QAction('Add frame') a.setObjectName('AddFrame') a.setShortcut('Ins') a.triggered.connect(self.__add_frame) self.actions['AddFrame'] = a def __create_insert_copy_frame_action(self): a = QtWidgets.QAction('Insert copy frame') a.setObjectName('InsertCopyFrame') a.setShortcut('Alt+Ins') a.triggered.connect(self.__insert_copy_frame) self.actions['InsertCopyFrame'] = a def __add_frame(self): self.model.appendRow() self.setCurrentIndex(self.model.index(len(self.model.Frames)-1,0)) self.__show_drawable() def __insert_copy_frame(self): idx = self.selectedIndexes() self.model.insertRow(idx[0].row(), self.model.Frames[idx[0].row()].pixmap.copy()) self.__show_drawable()
class FrameListModel(QtCore.QAbstractListModel): ... def appendRow(self, pixmap=None): self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount()) self.frames.append(Frame(pixmap)) self.endInsertRows() def insertRow(self, index, pixmap=None): self.beginInsertRows(QtCore.QModelIndex(), index, index) self.frames.insert(index, Frame(pixmap)) self.endInsertRows()
フレーム追加後の項目選択
フレーム追加後、自動的にその項目を選択するようにした。いちいちマウスクリックする必要がなくなった。
ショートカットキー変更
キー | 意味 |
---|---|
Ins |
透明な新規フレームを末尾に追加する |
Alt +Ins |
選択しているフレームのコピーを直後に追加する |
Del |
選択しているフレームを削除する |
バグ
- DnDによるファイル読込が機能しない(Pixmap化に伴う変更により)
所感
次はフレーム移動のショートカットキーを実装したい。できればこれをマウスでもできるようにしたい。DnDイベントの実装になりそうだが、イベント周りなのでまた意味不明なバグに悩まされる予感。
キー入力についてはキーコンフィグでユーザが自由に設定できるようにしたい。いつかきっと。でも重複なく設定するのが大変。なのでデフォルト設定をいくつか用意するのがベスト。Windows風、Vim風など。
あーでもその前にバグ改修か? やるべきことが沢山ある。
対象環境
- Raspbierry pi 4 Model B
- Raspbian buster 10.0 2019-09-26 ※
- bash 5.0.3(1)-release
- Qt 5.11
- Python 3.7.3
- PySide2
- Pillow 7.1.2
$ uname -a Linux raspberrypi 4.19.97-v7l+ #1294 SMP Thu Jan 30 13:21:14 GMT 2020 armv7l GNU/Linux