Skip to content

Instantly share code, notes, and snippets.

@wakarase
Last active April 23, 2023 15:04
Show Gist options
  • Save wakarase/609cd330e3e32e9b86882fc006fc4f92 to your computer and use it in GitHub Desktop.
Save wakarase/609cd330e3e32e9b86882fc006fc4f92 to your computer and use it in GitHub Desktop.
# バイナリエディタ🙂
import ctypes, hashlib, math, os, random, sys
import PySide6.QtCore as C
import PySide6.QtGui as G
import PySide6.QtWidgets as W
MONOSPACE_FONT = 'MS ゴシック'
class Application(W.QApplication):
"""アプリ全体を表す抽象的なクラスです。"""
def __init__(self):
super().__init__(sys.argv)
self.window = MainWindow()
AUTOMATIC_LOAD_PATH = 'example.bin'
class MainWindow(W.QMainWindow):
"""アプリのウィンドウです。"""
def __init__(self):
super().__init__()
self.resize(640, 480)
self.setAcceptDrops(True)
self.setWindowIcon(self.style().standardIcon(W.QStyle.SP_DriveFDIcon))
self.actionFileNew = ActionFileNew(self)
self.actionFileOpen = ActionFileOpen(self)
self.actionFileSave = ActionFileSave(self)
self.addAction(ActionMoveDocumentEnd(self))
self.addAction(ActionMoveDocumentStart(self))
self.addAction(ActionMoveDown(self))
self.addAction(ActionMoveLeft(self))
self.addAction(ActionMoveLineEnd(self))
self.addAction(ActionMoveLineStart(self))
self.addAction(ActionMoveRight(self))
self.addAction(ActionMoveUp(self))
self.tabWidget = TabWidget(self)
self.setMenuBar(MenuBar(self))
self.addToolBar(ToolBar(self))
self.setCentralWidget(CentralWidget(self))
self.setStatusBar(StatusBar(self))
self.show()
self.open(AUTOMATIC_LOAD_PATH) # 開発用です。
self.updateTitle()
def closeEvent(self, event):
print('Bye.')
def dragEnterEvent(self, event):
"""他のアプリからウィンドウ内に何かドラッグされると呼ばれます。"""
if event.mimeData().hasUrls():
event.accept() # dropEvent()が呼ばれるのに必要です。
else:
event.ignore()
def dropEvent(self, event):
"""ドラッグ&ドロップされたファイルらを開きます。"""
for url in event.mimeData().urls():
self.open(url.toLocalFile())
def keyPressEvent(self, event):
numberKeys = [C.Qt.Key_0, C.Qt.Key_1, C.Qt.Key_2, C.Qt.Key_3, C.Qt.Key_4, C.Qt.Key_5, C.Qt.Key_6, C.Qt.Key_7, C.Qt.Key_8, C.Qt.Key_9, C.Qt.Key_A, C.Qt.Key_B, C.Qt.Key_C, C.Qt.Key_D, C.Qt.Key_E, C.Qt.Key_F]
def isNumber(event):
""""押されたのが数字のキーなら真です。"""
return event.key() in numberKeys
def getNumber(event):
"""押されたキーの数字を返します。"""
return numberKeys.index(event.key())
if isNumber(event):
tab = self.tabWidget.currentWidget()
if not tab:
return
if not tab.editModeGet():
tab.editModeSet(getNumber(event))
else:
firstHex = tab.editModeGet()
secondHex = getNumber(event)
newByte = firstHex * 0x10 + secondHex
offset = tab.cursorStartGet()
if offset == len(tab.bytes):
tab.bytes.append(newByte)
tab.scrollBar.onNBytesUpdated()
else:
tab.bytes[offset] = newByte
tab.editModeSet(None)
tab.cursorStartSet(offset + 1)
tab.cursorEndSet(offset + 1)
tab.scrollBar.updateVisual()
def open(self, path):
"""与えられたpathのファイルを開きます。"""
self.tabWidget.open(path)
def updateTitle(self):
def createTitle():
s = 'ばいなりえでぃた'
tab = self.tabWidget.currentWidget()
if not tab:
return s
start, end = tab.cursorStartGet(), tab.cursorEndGet()
if start == end:
s += ' 0x{:X}'.format(start)
else:
s += ' 0x{:X}-0x{:X}'.format(start, end)
nBytes = len(tab.bytes)
s += ' / 0x{:X} ({:,} bytes)'.format(nBytes, nBytes)
return s
self.setWindowTitle(createTitle())
class StatusBar(W.QStatusBar):
"""下端に表示するステータスバーです。"""
def __init__(self, mainWindow):
super().__init__(mainWindow)
class MenuBar(W.QMenuBar):
"""メニューバーです。"""
def __init__(self, mainWindow):
super().__init__(mainWindow)
self.fileMenu = FileMenu(mainWindow)
self.addMenu(self.fileMenu)
class FileMenu(W.QMenu):
"""ファイルメニューです。"""
def __init__(self, mainWindow):
super().__init__('File', mainWindow)
self.addAction(mainWindow.actionFileNew)
self.addAction(mainWindow.actionFileOpen)
self.addAction(ActionFileReload(mainWindow))
self.addAction(mainWindow.actionFileSave)
self.addAction(ActionExit(mainWindow))
class ToolBar(W.QToolBar):
"""メニューバーの下に表示されるツールバーです。"""
def __init__(self, mainWindow):
super().__init__(mainWindow)
self.addAction(mainWindow.actionFileNew)
self.addAction(mainWindow.actionFileOpen)
self.addAction(mainWindow.actionFileSave)
class CentralWidget(W.QWidget):
"""ウィンドウの中央に表示されるウィジェットです。"""
def __init__(self, mainWindow):
super().__init__(mainWindow)
hbox = W.QHBoxLayout()
hbox.addWidget(mainWindow.tabWidget)
self.setLayout(hbox)
class TabWidget(W.QTabWidget):
"""複数のタブを表します。"""
def __init__(self, mainWindow):
self.mainWindow = mainWindow
super().__init__(mainWindow)
self.n_untitiled = 0
self.setMovable(True) # タブの順序をドラッグで移動できます。
self.setTabsClosable(True) # タブにクローズボタンをつけます。
self.currentChanged.connect(self.onCurrentChanged)
self.tabCloseRequested.connect(self.onTabCloseRequested)
def addTab(self, path):
"""新しくタブを追加します。"""
if path:
tab = Tab(self.mainWindow, path)
basename = os.path.basename(path)
i = super().addTab(tab, basename)
else:
self.n_untitiled += 1
tab = Tab(self.mainWindow, None)
i = super().addTab(tab, 'Untitled-{}'.format(self.n_untitiled))
self.setCurrentIndex(i)
def moveCursor(self, dx, dy):
"""カーソルを左右にdx、上下にdyだけ移動します。"""
tab = self.mainWindow.tabWidget.currentWidget()
if not tab:
return
if dx == -math.inf:
next = tab.cursorStartGet() // tab.nColumns * tab.nColumns
elif dx == math.inf:
next = (tab.cursorStartGet() // tab.nColumns + 1) * tab.nColumns - 1
next = min(next, len(tab.bytes))
elif dy == -math.inf:
next = 0
elif dy == math.inf:
next = len(tab.bytes)
else:
delta = dy * tab.nColumns + dx
if delta < 0:
next = tab.cursorStartGet() + delta
else:
next = tab.cursorEndGet() + delta
if next < 0 or len(tab.bytes) < next:
return
tab.cursorStartSet(next)
tab.cursorEndSet(next)
tab.scrollBar.showOffset(next)
tab.scrollBar.updateVisual()
def onCurrentChanged(self, i):
print('onCurrentChanged', i)
def onTabCloseRequested(self, i):
self.removeTab(i)
def open(self, path):
# すでに開かれているファイルなら、そのタブをアクティブにするだけです。
path = os.path.realpath(path) # パスの表現を一意にします。
for tab in self.tabs():
if tab.path == path:
self.setCurrentWidget(tab)
return
self.addTab(path)
def tabs(self):
tabs = []
for i in range(self.count()):
tabs.append(self.widget(i))
return tabs
class Tab(W.QWidget):
"""一つのタブの内容全体を表します。"""
def __init__(self, mainWindow, path):
self.mainWindow = mainWindow
super().__init__(mainWindow)
self.bytes = bytearray(b'')
self._cursorStart = 0
self._cursorEnd = 0
self.edited = False
self._editMode = None
self.nColumns = 0x10
self.nRows = 0x10
self.path = path
if path:
self.path = os.path.realpath(path)
self.read()
else:
self.path = None
hbox = W.QHBoxLayout()
self.setLayout(hbox)
self.grid = W.QGridLayout()
self.grid.setSpacing(0)
hbox.addLayout(self.grid)
def addLabel(y, x, s, color=None):
label = W.QLabel(s, self)
label.setContentsMargins(2, 2, 2, 2)
font = G.QFont(MONOSPACE_FONT)
font.setPointSizeF(font.pointSizeF())
label.setFont(font)
if color:
label.setStyleSheet('color: {};\n'.format(color))
self.grid.addWidget(label, y, x)
addLabel(0, 0, 'Address', color='blue') # 左上
addLabel(0, self.nColumns + 2, 'UTF-8 Decoded', color='blue') # 右上
for i in range(self.nColumns): # 上のアドレス表示
addLabel(0, i + 1, '{:02X}'.format(i), color='blue')
for i in range(self.nRows): # 左のアドレス表示
addLabel(i + 1, 0, '{:08X}'.format(self.nColumns * i), color='blue')
for y in range(self.nRows):
for x in range(self.nColumns):
# 各バイト値を表示する部分です。
n = random.randint(0x00, 0xff)
addLabel(y + 1, x + 1, '{:02X}'.format(n))
for y in range(self.nRows + 1): # レイアウトを調整するためのダミー列です。
addLabel(y, self.nColumns + 1, '')
for y in range(self.nRows):
addLabel(y + 1, self.nColumns + 2, 'あいうえおかきく', color='gray')
hbox.addStretch()
vbox = W.QVBoxLayout()
hbox.addLayout(vbox)
vbox.setSpacing(0)
vbox.setAlignment(C.Qt.AlignHCenter)
def addToolButton(layout, action):
button = W.QToolButton()
layout.setAlignment(button, C.Qt.AlignHCenter)
button.setAutoRepeat(True)
button.setDefaultAction(action)
button.setStyleSheet('border: none')
layout.addWidget(button)
addToolButton(vbox, ActionPageUp(mainWindow))
addToolButton(vbox, ActionLineUp(mainWindow))
self.scrollBar = ScrollBar(self)
vbox.addWidget(self.scrollBar)
vbox.setAlignment(self.scrollBar, C.Qt.AlignHCenter)
addToolButton(vbox, ActionLineDown(mainWindow))
addToolButton(vbox, ActionPageDown(mainWindow))
def cursorEndGet(self):
return self._cursorEnd
def cursorEndSet(self, x):
self._cursorEnd = x
self.mainWindow.updateTitle()
def cursorStartGet(self):
return self._cursorStart
def cursorStartSet(self, x):
self._cursorStart = x
self.mainWindow.updateTitle()
def editModeGet(self):
return self._editMode
def editModeSet(self, x):
self._editMode = x
start, end = self.cursorStartGet(), self.cursorEndGet()
if start != end:
self.cursorEndSet(start)
self.scrollBar.updateVisual()
def getOffset(self, y, x):
"""QGridLayout上の位置からバイトオフセットを得ます。"""
offset = self.nColumns * self.scrollBar.value()
return offset + (y - 1) * self.nColumns + (x - 1)
def mouseMoveEvent(self, event):
if not self.mousePressItemPosition:
return
child = self.childAt(event.position().toPoint())
if isinstance(child, W.QLabel):
index = self.grid.indexOf(child)
position = self.grid.getItemPosition(index)
y, x, _, _ = position
if 1 <= x <= self.nColumns and 1 <= y <= self.nRows:
mouseMoveItemPosition = y, x
a = self.getOffset(*self.mousePressItemPosition)
b = self.getOffset(*mouseMoveItemPosition)
self.cursorStartSet(min(a, b))
self.cursorEndSet(max(a, b))
self.scrollBar.updateVisual()
def mousePressEvent(self, event):
self.mousePressItemPosition = None
child = self.childAt(event.position().toPoint())
if isinstance(child, W.QLabel):
index = self.grid.indexOf(child)
position = self.grid.getItemPosition(index)
y, x, _, _ = position
if 1 <= x <= self.nColumns and 1 <= y <= self.nRows:
self.mousePressItemPosition = y, x
event.accept()
def mouseReleaseEvent(self, event):
if not self.mousePressItemPosition:
return
mousePressItemPosition = self.mousePressItemPosition
self.mousePressItemPosition = None
child = self.childAt(event.position().toPoint())
if isinstance(child, W.QLabel):
index = self.grid.indexOf(child)
position = self.grid.getItemPosition(index)
y, x, _, _ = position
if 1 <= x <= self.nColumns and 1 <= y <= self.nRows:
mouseReleaseItemPosition = y, x
a = self.getOffset(*mousePressItemPosition)
b = self.getOffset(*mouseReleaseItemPosition)
self.cursorStartSet(min(a, b))
self.cursorEndSet(max(a, b))
self.scrollBar.updateVisual()
def read(self):
"""バイナリファイルを読み込みます。"""
if not self.path:
return
s = 'Opening a file as binary. path="{}" size(bytes)={:,}'
s = s.format(self.path, os.path.getsize(self.path))
print(s)
with open(self.path, 'rb') as file:
self.bytes = bytearray(file.read())
s = 'Successfully loaded {:,} bytes into memory.'
s = s.format(len(self.bytes))
print(s)
self.sha1 = hashlib.sha1(self.bytes).hexdigest()
s = 'SHA-1: {}'.format(self.sha1)
print(s)
def write(self):
"""バイナリファイルに書き込みます。"""
if not self.path:
return
if os.path.exists(self.path):
with open(self.path, 'rb') as file:
fileBytes = file.read()
sha1 = hashlib.sha1(fileBytes).hexdigest()
if sha1 != self.sha1:
print('Warning: SHA-1 of the file changed.')
s = 'Writing binary to the file. path="{}" size(bytes)={:,}'
s = s.format(self.path, len(self.bytes))
print(s)
with open(self.path, 'wb') as file:
file.write(self.bytes)
s = 'Successfully wrote {:,} bytes into the file.'
s = s.format(os.path.getsize(self.path))
print(s)
class ScrollBar(W.QScrollBar):
"""タブ内の右に表示される縦長のスクロールバーです。"""
def __init__(self, tab):
self.tab = tab
super().__init__(tab)
self.valueChanged.connect(self.onValueChanged)
self.onNBytesUpdated()
def getVisibleOffset(self):
"""第startバイトから第end - 1バイトまで表示されます。"""
start = self.value() * self.tab.nColumns
end = (self.value() + self.pageStep()) * self.tab.nColumns
return start, end
def isOffsetVisible(self, i):
"""第iバイトが見えていれば真です。"""
start, end = self.getVisibleOffset()
return start <= i < end
def moveLine(self, delta):
self.setValue(self.value() + delta)
def movePage(self, delta):
self.setValue(self.value() + delta * self.pageStep())
def onNBytesUpdated(self):
tab = self.tab
nBytes = len(tab.bytes)
nColumns = tab.nColumns
nRows = (nBytes + nColumns - 1) // nColumns
maximum = max(0, nRows - tab.nRows)
self.setPageStep(tab.nRows)
self.setMaximum(maximum)
self.updateVisual()
def onValueChanged(self, value):
self.updateVisual()
def showOffset(self, i):
"""第iバイトが見えるようにスクロールを修正します。"""
start, end = self.getVisibleOffset()
if i < start: # 表示したい位置が表示されている位置より前にあります。
self.setValue(i // self.tab.nColumns)
elif i >= end: # 表示したい位置が表示されている位置より後にあります。
self.setValue(i // self.tab.nColumns - self.pageStep() + 1)
def updateVisual(self):
"""タブ内の表示内容を更新します。"""
tab = self.tab
length = len(tab.bytes)
start, _ = self.getVisibleOffset()
for y in range(tab.nRows): # 左端列のアドレス表示です。
label = tab.grid.itemAtPosition(y + 1, 0).widget()
i = start + y * tab.nColumns
if i > length:
# データがある行についてだけアドレスを表示します。
# ただし、行末までデータがある場合は、次行でも表示します。
label.setText(' ')
else:
label.setText('{:08X}'.format(i))
for y in range(tab.nRows):
for x in range(tab.nColumns):
label = tab.grid.itemAtPosition(y + 1, x + 1).widget()
i = start + y * tab.nColumns + x
if i >= length:
label.setText(' ')
else:
label.setText('{:02X}'.format(tab.bytes[i]))
if tab.cursorStartGet() <= i <= tab.cursorEndGet():
label.setStyleSheet('background-color: yellow')
else:
label.setStyleSheet('')
if tab.editModeGet(): # 編集中のバイトを表示します。
offset = tab.cursorStartGet()
if self.isOffsetVisible(offset):
offset -= start
y = offset // tab.nColumns
x = offset % tab.nColumns
label = tab.grid.itemAtPosition(y + 1, x + 1).widget()
label.setText('{:X}_'.format(tab.editModeGet()))
label.setStyleSheet('background-color: pink')
for y in range(tab.nRows): # 右側の文字デコード表示です。
label = tab.grid.itemAtPosition(y + 1, tab.nColumns + 2).widget()
s = ''
i = start + y * tab.nColumns
nextLineHead = start + (y + 1) * tab.nColumns
while i < nextLineHead:
isDecodeSuccess = False
nDecodedBytes = 0
for nBytes in [1, 2, 3, 4]:
if i + nBytes > length:
continue
bytes = tab.bytes[i:i + nBytes]
try:
character = bytes.decode('utf-8')
except UnicodeError:
continue
if '\u0000' <= character <= '\u001f':
# https://en.wikipedia.org/wiki/Unicode_control_characters
# control pictureというものが0x2400番台に定義されています。
n = ord(character)
n += 0x2400 # 制御文字を対応する記号に置き換えます。
character = chr(n)
s += character
isDecodeSuccess = True
nDecodedBytes = nBytes
break
if isDecodeSuccess:
i += nDecodedBytes
else:
i += 1
label.setText(s)
class Action(G.QAction):
"""継承して利用するための共通部分です。"""
def __init__(self, mainWindow, text, statusTip, shortcut=None, standardIcon=None):
self.mainWindow = mainWindow
if standardIcon:
icon = mainWindow.style().standardIcon(standardIcon)
super().__init__(icon, text, mainWindow)
else:
super().__init__(text, mainWindow)
self.setStatusTip(statusTip)
if shortcut:
self.setShortcut(G.QKeySequence(shortcut))
self.triggered.connect(self.run)
def run(self):
raise NotImplementedError()
class ActionExit(Action):
def __init__(self, mainWindow):
super().__init__(mainWindow, 'Exit Program', 'プログラムを終了します。', 'Escape')
def run(self):
self.mainWindow.close()
class ActionFileNew(Action):
def __init__(self, mainWindow):
super().__init__(mainWindow, 'New', '新規作成します。', None, W.QStyle.SP_FileIcon)
def run(self):
self.mainWindow.tabWidget.addTab(None)
class ActionFileOpen(Action):
def __init__(self, mainWindow):
super().__init__(mainWindow, 'Open File...', 'ファイルを開きます。', None, W.QStyle.SP_DialogOpenButton)
def run(self):
fileName, _ = W.QFileDialog.getOpenFileName(self.mainWindow)
if fileName != '':
self.mainWindow.open(fileName)
class ActionFileReload(Action):
def __init__(self, mainWindow):
super().__init__(mainWindow, 'Reload File', 'ファイルを再読み込みします。', 'F5')
def run(self):
tab = self.mainWindow.tabWidget.currentWidget()
tab.read()
class ActionFileSave(Action):
def __init__(self, mainWindow):
super().__init__(mainWindow, 'Save File...', 'ファイルに保存します。', None, W.QStyle.SP_DialogSaveButton)
def run(self):
tab = self.mainWindow.tabWidget.currentWidget()
if not tab.path:
fileName, _ = W.QFileDialog.getSaveFileName(self.mainWindow, dir='untitled.bin')
if fileName == '':
return
tab.path = os.path.realpath(fileName)
tab.write()
class ActionLineDown(Action):
def __init__(self, mainWindow):
super().__init__(mainWindow, 'Line Down', '1行下にスクロールします。', None, W.QStyle.SP_TitleBarUnshadeButton)
def run(self):
tab = self.mainWindow.tabWidget.currentWidget()
value = tab.scrollBar.value()
tab.scrollBar.setValue(value + 1)
class ActionLineUp(Action):
def __init__(self, mainWindow):
super().__init__(mainWindow, 'Line Up', '1行上にスクロールします。', None, W.QStyle.SP_TitleBarShadeButton)
def run(self):
tab = self.mainWindow.tabWidget.currentWidget()
value = tab.scrollBar.value()
tab.scrollBar.setValue(value - 1)
class ActionMoveDocumentEnd(Action):
def __init__(self, mainWindow):
super().__init__(mainWindow, 'Move to Document End', '下端に移動します。', 'Ctrl+End')
def run(self):
self.mainWindow.tabWidget.moveCursor(0, math.inf)
class ActionMoveDocumentStart(Action):
def __init__(self, mainWindow):
super().__init__(mainWindow, 'Move to Document Start', '上端に移動します。', 'Ctrl+Home')
def run(self):
self.mainWindow.tabWidget.moveCursor(0, -math.inf)
class ActionMoveDown(Action):
def __init__(self, mainWindow):
super().__init__(mainWindow, 'Move Up', '下に1文字移動します。', 'Down')
def run(self):
self.mainWindow.tabWidget.moveCursor(0, 1)
class ActionMoveLeft(Action):
def __init__(self, mainWindow):
super().__init__(mainWindow, 'Move Left', '左に1文字移動します。', 'Left')
def run(self):
self.mainWindow.tabWidget.moveCursor(-1, 0)
class ActionMoveLineEnd(Action):
def __init__(self, mainWindow):
super().__init__(mainWindow, 'Move to Line End', '行末に移動します。', 'End')
def run(self):
self.mainWindow.tabWidget.moveCursor(math.inf, 0)
class ActionMoveLineStart(Action):
def __init__(self, mainWindow):
super().__init__(mainWindow, 'Move to Line Start', '行頭に移動します。', 'Home')
def run(self):
self.mainWindow.tabWidget.moveCursor(-math.inf, 0)
class ActionMoveRight(Action):
def __init__(self, mainWindow):
super().__init__(mainWindow, 'Move Right', '右に1文字移動します。', 'Right')
def run(self):
self.mainWindow.tabWidget.moveCursor(1, 0)
class ActionMoveUp(Action):
def __init__(self, mainWindow):
super().__init__(mainWindow, 'Move Up', '上に1文字移動します。', 'Up')
def run(self):
self.mainWindow.tabWidget.moveCursor(0, -1)
class ActionPageDown(Action):
def __init__(self, mainWindow):
super().__init__(mainWindow, 'Page Down', '1ページ下にスクロールします。', 'PgDown', W.QStyle.SP_ArrowDown)
def run(self):
tab = self.mainWindow.tabWidget.currentWidget()
tab.scrollBar.movePage(1)
class ActionPageUp(Action):
def __init__(self, mainWindow):
super().__init__(mainWindow, 'Page Up', '1ページ上にスクロールします。', 'PgUp', W.QStyle.SP_ArrowUp)
def run(self):
tab = self.mainWindow.tabWidget.currentWidget()
tab.scrollBar.movePage(-1)
if __name__ == '__main__':
# https://stackoverflow.com/questions/1551605
# How to set application's taskbar icon in Windows 7
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID('myhexeditor')
application = Application()
sys.exit(application.exec())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment