PySide6でキーボードとマウスの入力内容を表示するアプリを作ってみました

2023/09/23 categories:Python| tags:Python|PySide6|pynput|

PySide6にpynputで取得したキーボードとマウスの入力内容を表示するアプリを作ってみました。

動作の様子

pynputでキーボードやマウスの入力を監視して、PySide6に入力内容を表示できるようにしました。表示設定内度は右クリックで表示されるコンテキストメニューの設定から設定できます。

ソースコード

keyboard_mouse_monitor.py

import sys
import pynput
import json
from pathlib import Path
from PySide6 import QtWidgets, QtCore, QtGui

from settings_dialog import SettingsDialog


class Mainwindow(QtWidgets.QMainWindow):
    def __init__(self, app) -> None:
        super().__init__()

        self.app = app
        self.font_size = 12
        self.max_row = 3
        self.is_stays_on_top = False
        self.is_frameless = False
        self.foreground_color = '#000000'
        self.background_color = '#FFFFFF'
        self.keymap = {}
        self.mousemap = {}
        self.settings_path = Path(__file__).parent / 'settings.json'

        self.model = QtGui.QStandardItemModel()
        self.view = View(self.model)
        self.view.dragged.connect(self.move_window)
        self.view.customContextMenuRequested.connect(self.show_context_menu)
        self.setCentralWidget(self.view)

        self.setWindowTitle('入力表示')
        self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowType.WindowMinimizeButtonHint)
        self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowType.WindowMaximizeButtonHint)
        pixmap = QtGui.QPixmap(1, 1)
        pixmap.fill(QtCore.Qt.GlobalColor.transparent)
        self.setWindowIcon(QtGui.QIcon(pixmap))

        self.load_settings_from_file()

        self.mouse_and_key_monitor = MouseAndKeyMonitor()
        self.mouse_and_key_monitor.key_changed.connect(self.key_changed)
        self.mouse_and_key_monitor.mouse_changed.connect(self.mouse_changed)
        self.mouse_and_key_monitor.start()

    def load_settings(self, settings):
        self.is_stays_on_top = settings['stays_on_top']
        self.is_frameless = settings['frameless']
        self.font_size = settings['font_size']
        self.max_row = settings['max_row']
        self.mousemap = settings['mousemap']
        self.keymap = settings['keymap']
        self.resize(settings['window_width'], settings['window_height'])
        self.foreground_color = settings['foreground_color']
        self.background_color = settings['background_color']
        self.change_window_flag(self.is_stays_on_top, QtCore.Qt.WindowType.WindowStaysOnTopHint)
        self.change_window_flag(self.is_frameless, QtCore.Qt.WindowType.FramelessWindowHint)
        self.model.removeRows(0, self.model.rowCount())

    def load_settings_from_file(self):
        try:
            with open(self.settings_path, mode='r', encoding='utf-8') as f:
                settings = json.load(f)
            self.load_settings(settings)
        except:
            settings = DEFAULT_SETTINGS
            self.load_settings(settings)
        self.change_window_flag(self.is_stays_on_top, QtCore.Qt.WindowType.WindowStaysOnTopHint)
        self.change_window_flag(self.is_frameless, QtCore.Qt.WindowType.FramelessWindowHint)

    def settings(self):
        settings = {}
        settings['stays_on_top'] = self.is_stays_on_top
        settings['frameless'] = self.is_frameless
        settings['font_size'] = self.font_size
        settings['max_row'] = self.max_row
        settings['window_width'] = self.width()
        settings['window_height'] = self.height()
        settings['foreground_color'] = self.foreground_color
        settings['background_color'] = self.background_color
        settings['mousemap'] = self.mousemap
        settings['keymap'] = self.keymap
        return settings

    def show_settings(self):
        dialog = SettingsDialog(self.app, self.settings())
        if not dialog.exec():
            return
        settings = dialog.to_dict()
        try:
            self.load_settings(settings)
        except:
            settings = DEFAULT_SETTINGS
            self.load_settings(settings)

    def closeEvent(self, event: QtGui.QCloseEvent) -> None:
        settings = self.settings()
        with open(self.settings_path, mode='w', encoding='utf-8') as f:
            json.dump(settings, f, indent=4, ensure_ascii=False)
        return super().closeEvent(event)

    def change_window_flag(self, is_set: bool, window_type: QtCore.Qt.WindowType):
        if is_set:
            self.setWindowFlags(self.windowFlags() | window_type)
        else:
            self.setWindowFlags(self.windowFlags() & ~window_type)
        self.show()

    def show_context_menu(self, pos):

        def toggle_stays_on_top():
            self.is_stays_on_top = not self.is_stays_on_top
            self.change_window_flag(self.is_stays_on_top, QtCore.Qt.WindowType.WindowStaysOnTopHint)

        def toggle_frameless():
            self.is_frameless = not self.is_frameless
            self.change_window_flag(self.is_frameless, QtCore.Qt.WindowType.FramelessWindowHint)

        stays_on_top = QtGui.QAction('常に最前面に表示')
        stays_on_top.setCheckable(True)
        stays_on_top.setChecked(self.is_stays_on_top)
        stays_on_top.triggered.connect(toggle_stays_on_top)
        
        frameless = QtGui.QAction('ウィンドウフレームを非表示')
        frameless.setCheckable(True)
        frameless.setChecked(self.is_frameless)
        frameless.triggered.connect(toggle_frameless)
        
        menu = QtWidgets.QMenu()
        menu.addAction(stays_on_top)
        menu.addAction(frameless)
        menu.addSeparator()
        menu.addAction('設定', self.show_settings)
        menu.addSeparator()
        menu.addAction('終了', self.close)
        menu.exec(self.mapToGlobal(pos))

    def move_window(self):
        self.windowHandle().startSystemMove()

    def key_changed(self, key_codes: list[int]):
        key_names = [ self.keymap[str(i)][0] for i in key_codes ]
        self.append_text(' + '.join(key_names))

    def mouse_changed(self, mouse: dict):
        texts = []
        for button, action in mouse.items():
            texts.append(self.mousemap['buttons'][button] + self.mousemap['actions'][action])
        self.append_text(' + '.join(texts))

    def append_text(self, text):
        item = self.item(text)
        self.model.insertRow(0, item)
        if self.model.rowCount() > self.max_row:
            self.model.removeRow(self.model.rowCount() - 1)
        self.view.setCurrentIndex(self.model.index(0, 0))

    def item(self, text):
        item = QtGui.QStandardItem()
        item.setEnabled(False)
        item.setEditable(False)
        item.setSelectable(False)
        item.setText(text)
        item.setData(QtGui.QColor(self.foreground_color), QtCore.Qt.ItemDataRole.ForegroundRole)
        item.setData(QtGui.QColor(self.background_color), QtCore.Qt.ItemDataRole.BackgroundRole)
        font = item.font()
        font.setPointSize(self.font_size)
        item.setFont(font)
        return item


class MouseAndKeyMonitor(QtCore.QObject):

    key_changed = QtCore.Signal(list)
    mouse_changed = QtCore.Signal(dict)

    def __init__(self) -> None:
        super().__init__()
        self.key_pressing = []
        self.mouse = {}

    def start(self):
        self.mouser_listener = pynput.mouse.Listener(on_move=self.on_move, on_click=self.on_click, on_scroll=self.on_scroll)
        self.mouser_listener.start()
        self.keyboard_listener = pynput.keyboard.Listener(on_press=self.on_press,on_release=self.on_release)
        self.keyboard_listener.start()

    def stop(self):
        self.mouser_listener.stop()
        self.keyboard_listener.stop()

    def on_move(self, x, y):
        changed = False
        for key in self.mouse:
            if self.mouse[key] != 'drag':
                self.mouse[key] = 'drag'
                changed = True
        if changed:
            self.mouse_changed.emit(self.mouse)

    def on_click(self, x, y, button: pynput.mouse.Button, pressed):
        button_name = button.name
        if pressed:
            if not button_name in self.mouse:
                self.mouse[button_name] = 'press'
                self.mouse_changed.emit(self.mouse)
        else:
            if button_name in self.mouse:
                del self.mouse[button_name]
            #self.mouse_changed.emit(self.mouse)

    def on_scroll(self, x, y, dx, dy):
        action = 'up' if dy > 0 else 'down'
        self.mouse['scroll'] = action
        self.mouse_changed.emit(self.mouse)
        del self.mouse['scroll']

    def on_press(self, key):
        key_code = key.vk if type(key) is pynput.keyboard._win32.KeyCode else key.value.vk
        if not key_code in self.key_pressing:
            self.key_pressing.append(key_code)
            self.key_changed.emit(self.key_pressing)

    def on_release(self, key):
        key_code = key.vk if type(key) is pynput.keyboard._win32.KeyCode else key.value.vk
        del self.key_pressing[self.key_pressing.index(key_code)]
        #self.key_changed.emit(self.key_pressing)


class View(QtWidgets.QListView):
    dragged = QtCore.Signal()
    def __init__(self, model) -> None:
        super().__init__()
        self.setModel(model)
        self.setMinimumHeight(1)
        self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu)

    def mouseMoveEvent(self, event: QtGui.QMouseEvent) -> None:
        if event.buttons() == QtCore.Qt.MouseButton.LeftButton:
            self.dragged.emit()
        return super().mouseMoveEvent(event)


DEFAULT_SETTINGS = {
    'stays_on_top':False,
    'frameless':False,
    'font_size':12,
    'max_row':3,
    'window_width':300,
    'window_height':100,
    'foreground_color': '#000000',
    'background_color': '#FFFFFF',
    'mousemap':{
        'buttons':{'left':'左', 'right':'右', 'middle':'中', 'scroll':'スクロール', 'x1':'x1', 'x2':'x2'},
        'actions':{'press' : 'クリック', 'drag' : 'ドラッグ', 'up' : '上', 'down' : '下'}
    },
    'keymap':{
        '0' : ['', ''],
        '1' : ['', 'マウスの左ボタン'],
        '2' : ['', 'マウスの右ボタン'],
        '3' : ['', 'コントロール・ブレイク処理'],
        '4' : ['', 'マウスの中央ボタン(3ボタンマウス)'],
        '5' : ['', 'マウスの第1拡張ボタン'],
        '6' : ['', 'マウスの第2拡張ボタン'],
        '7' : ['', '未定義'],
        '8' : ['Backspace', 'Backspaceキー'],
        '9' : ['Tab', 'Tabキー'],
        '10' : ['', '予約済'],
        '11' : ['', '予約済'],
        '12' : ['Clear', 'Clearキー'],
        '13' : ['Enter', 'Enterキー'],
        '14' : ['', '未定義'],
        '15' : ['', '未定義'],
        '16' : ['Shift', 'Shiftキー'],
        '17' : ['Ctrl', 'Ctrlキー'],
        '18' : ['Alt', 'Altキー'],
        '19' : ['Pause', 'Pauseキー'],
        '20' : ['CapsLock', 'CapsLockキー'],
        '21' : ['', 'IME かなモード'],
        '22' : ['', '未定義'],
        '23' : ['', 'IME Junjaモード'],
        '24' : ['', 'IME ファイナルモード'],
        '25' : ['', 'IME 漢字モード'],
        '26' : ['', '未定義'],
        '27' : ['Esc', 'Escキー'],
        '28' : ['', 'IME 変換'],
        '29' : ['', 'IME 無変換'],
        '30' : ['', 'IME 使用可能'],
        '31' : ['', 'IME モード変更要求'],
        '32' : ['スペース', 'スペースキー'],
        '33' : ['PageUp', 'PageUpキー'],
        '34' : ['PageDown', 'PageDownキー'],
        '35' : ['End', 'Endキー'],
        '36' : ['Home', 'Homeキー'],
        '37' : ['←', '←キー'],
        '38' : ['↑', '↑キー'],
        '39' : ['→', '→キー'],
        '40' : ['↓', '↓キー'],
        '41' : ['Select', 'Selectキー'],
        '42' : ['Print', 'Printキー'],
        '43' : ['Execute', 'Executeキー'],
        '44' : ['PrintScreen', 'PrintScreenキー'],
        '45' : ['Insert', 'Insertキー'],
        '46' : ['Delete', 'Deleteキー'],
        '47' : ['Help', 'Helpキー'],
        '48' : ['0', '0キー'],
        '49' : ['1', '1キー'],
        '50' : ['2', '2キー'],
        '51' : ['3', '3キー'],
        '52' : ['4', '4キー'],
        '53' : ['5', '5キー'],
        '54' : ['6', '6キー'],
        '55' : ['7', '7キー'],
        '56' : ['8', '8キー'],
        '57' : ['9', '9キー'],
        '58' : ['', '未定義'],
        '59' : ['', '未定義'],
        '60' : ['', '未定義'],
        '61' : ['', '未定義'],
        '62' : ['', '未定義'],
        '63' : ['', '未定義'],
        '64' : ['', '未定義'],
        '65' : ['A', 'Aキー'],
        '66' : ['B', 'Bキー'],
        '67' : ['C', 'Cキー'],
        '68' : ['D', 'Dキー'],
        '69' : ['E', 'Eキー'],
        '70' : ['F', 'Fキー'],
        '71' : ['G', 'Gキー'],
        '72' : ['H', 'Hキー'],
        '73' : ['I', 'Iキー'],
        '74' : ['J', 'Jキー'],
        '75' : ['K', 'Kキー'],
        '76' : ['L', 'Lキー'],
        '77' : ['M', 'Mキー'],
        '78' : ['N', 'Nキー'],
        '79' : ['O', 'Oキー'],
        '80' : ['P', 'Pキー'],
        '81' : ['Q', 'Qキー'],
        '82' : ['R', 'Rキー'],
        '83' : ['S', 'Sキー'],
        '84' : ['T', 'Tキー'],
        '85' : ['U', 'Uキー'],
        '86' : ['V', 'Vキー'],
        '87' : ['W', 'Wキー'],
        '88' : ['X', 'Xキー'],
        '89' : ['Y', 'Yキー'],
        '90' : ['Z', 'Zキー'],
        '91' : ['左Windows', '左Windowsキー'],
        '92' : ['右Windows', '右Windowsキー'],
        '93' : ['アプリケーション', 'アプリケーションキー'],
        '94' : ['', '予約済'],
        '95' : ['コンピュータスリープ', 'コンピュータスリープキー'],
        '96' : ['0', 'テンキーの0キー'],
        '97' : ['1', 'テンキーの1キー'],
        '98' : ['2', 'テンキーの2キー'],
        '99' : ['3', 'テンキーの3キー'],
        '100' : ['4', 'テンキーの4キー'],
        '101' : ['5', 'テンキーの5キー'],
        '102' : ['6', 'テンキーの6キー'],
        '103' : ['7', 'テンキーの7キー'],
        '104' : ['8', 'テンキーの8キー'],
        '105' : ['9', 'テンキーの9キー'],
        '106' : ['*', 'テンキーの*キー'],
        '107' : ['+', 'テンキーの+キー'],
        '108' : ['区切り記号', '区切り記号キー'],
        '109' : ['減算記号', '減算記号キー'],
        '110' : ['小数点', '小数点キー'],
        '111' : ['除算記号', '除算記号キー'],
        '112' : ['F1', 'F1キー'],
        '113' : ['F2', 'F2キー'],
        '114' : ['F3', 'F3キー'],
        '115' : ['F4', 'F4キー'],
        '116' : ['F5', 'F5キー'],
        '117' : ['F6', 'F6キー'],
        '118' : ['F7', 'F7キー'],
        '119' : ['F8', 'F8キー'],
        '120' : ['F9', 'F9キー'],
        '121' : ['F10', 'F10キー'],
        '122' : ['F11', 'F11キー'],
        '123' : ['F12', 'F12キー'],
        '124' : ['F13', 'F13キー'],
        '125' : ['F14', 'F14キー'],
        '126' : ['F15', 'F15キー'],
        '127' : ['F16', 'F16キー'],
        '128' : ['F17', 'F17キー'],
        '129' : ['F18', 'F18キー'],
        '130' : ['F19', 'F19キー'],
        '131' : ['F20', 'F20キー'],
        '132' : ['F21', 'F21キー'],
        '133' : ['F22', 'F22キー'],
        '134' : ['F23', 'F23キー'],
        '135' : ['F24', 'F24キー'],
        '136' : ['', '割当無し'],
        '137' : ['', '割当無し'],
        '138' : ['', '割当無し'],
        '139' : ['', '割当無し'],
        '140' : ['', '割当無し'],
        '141' : ['', '割当無し'],
        '142' : ['', '割当無し'],
        '143' : ['', '割当無し'],
        '144' : ['NumLock', 'NumLockキー'],
        '145' : ['ScrollLock', 'ScrollLockキー'],
        '146' : ['', 'OEM固有'],
        '147' : ['', 'OEM固有'],
        '148' : ['', 'OEM固有'],
        '149' : ['', 'OEM固有'],
        '150' : ['', 'OEM固有'],
        '151' : ['', '割当無し'],
        '152' : ['', '割当無し'],
        '153' : ['', '割当無し'],
        '154' : ['', '割当無し'],
        '155' : ['', '割当無し'],
        '156' : ['', '割当無し'],
        '157' : ['', '割当無し'],
        '158' : ['', '割当無し'],
        '159' : ['', '割当無し'],
        '160' : ['左Shift', '左Shiftキー'],
        '161' : ['右Shift', '右Shiftキー'],
        '162' : ['左Ctrl', '左Ctrlキー'],
        '163' : ['右Ctrl', '右Ctrlキー'],
        '164' : ['左Alt', '左Altキー'],
        '165' : ['右Alt', '右Altキー'],
        '166' : ['ブラウザーの戻る', 'ブラウザーの戻るキー'],
        '167' : ['ブラウザーの進む', 'ブラウザーの進むキー'],
        '168' : ['ブラウザーの更新の', 'ブラウザーの更新のキー'],
        '169' : ['ブラウザーの停止', 'ブラウザーの停止キー'],
        '170' : ['ブラウザーの検索', 'ブラウザーの検索キー'],
        '171' : ['ブラウザーのお気に入り', 'ブラウザーのお気に入りキー'],
        '172' : ['ブラウザーの開始およびホーム', 'ブラウザーの開始およびホームキー'],
        '173' : ['音量ミュート', '音量ミュートキー'],
        '174' : ['音量ダウン', '音量ダウンキー'],
        '175' : ['音量アップ', '音量アップキー'],
        '176' : ['次のトラック', '次のトラックキー'],
        '177' : ['前のトラック', '前のトラックキー'],
        '178' : ['メディア停止', 'メディア停止キー'],
        '179' : ['メディア再生/一時停止', 'メディア再生/一時停止キー'],
        '180' : ['メール起動', 'メール起動キー'],
        '181' : ['メディア選択', 'メディア選択キー'],
        '182' : ['アプリケーション1起動', 'アプリケーション1起動キー'],
        '183' : ['アプリケーション2起動', 'アプリケーション2起動キー'],
        '184' : ['', '予約済'],
        '185' : ['', '予約済'],
        '186' : ['OEM1', 'OEM1キー'],
        '187' : ['+', '+キー'],
        '188' : [',', ',キー'],
        '189' : ['-', '-キー'],
        '190' : ['.', '.キー'],
        '191' : ['OEM2', 'OEM2キー'],
        '192' : ['OEM3', 'OEM3キー'],
        '193' : ['', '予約済'],
        '194' : ['', '予約済'],
        '195' : ['', '予約済'],
        '196' : ['', '予約済'],
        '197' : ['', '予約済'],
        '198' : ['', '予約済'],
        '199' : ['', '予約済'],
        '200' : ['', '予約済'],
        '201' : ['', '予約済'],
        '202' : ['', '予約済'],
        '203' : ['', '予約済'],
        '204' : ['', '予約済'],
        '205' : ['', '予約済'],
        '206' : ['', '予約済'],
        '207' : ['', '予約済'],
        '208' : ['', '予約済'],
        '209' : ['', '予約済'],
        '210' : ['', '予約済'],
        '211' : ['', '予約済'],
        '212' : ['', '予約済'],
        '213' : ['', '予約済'],
        '214' : ['', '予約済'],
        '215' : ['', '予約済'],
        '216' : ['', '割当無し'],
        '217' : ['', '割当無し'],
        '218' : ['', '割当無し'],
        '219' : ['OEM4', 'OEM4キー'],
        '220' : ['OEM5', 'OEM5キー'],
        '221' : ['OEM6', 'OEM6キー'],
        '222' : ['OEM7', 'OEM7キー'],
        '223' : ['OEM8', 'OEM8キー'],
        '224' : ['', '予約済'],
        '225' : ['', 'OEM固有'],
        '226' : ['OEM102', 'OEM102キー'],
        '227' : ['', 'OEM固有'],
        '228' : ['', 'OEM固有'],
        '229' : ['IME PROCESS', 'IME PROCESSキー'],
        '230' : ['', 'OEM固有'],
        '231' : ['', 'キーボード以外の入力手段に使用される32 ビット仮想キー値の下位ワード'],
        '232' : ['', '割当無し'],
        '233' : ['', 'OEM固有'],
        '234' : ['', 'OEM固有'],
        '235' : ['', 'OEM固有'],
        '236' : ['', 'OEM固有'],
        '237' : ['', 'OEM固有'],
        '238' : ['', 'OEM固有'],
        '239' : ['', 'OEM固有'],
        '240' : ['', 'OEM固有'],
        '241' : ['', 'OEM固有'],
        '242' : ['', 'OEM固有'],
        '243' : ['', 'OEM固有'],
        '244' : ['', 'OEM固有'],
        '245' : ['', 'OEM固有'],
        '246' : ['Attn', 'Attnキー'],
        '247' : ['CrSel', 'CrSelキー'],
        '248' : ['ExSel', 'ExSelキー'],
        '249' : ['Erase EOF', 'Erase EOFキー'],
        '250' : ['Play', 'Playキー'],
        '251' : ['Zoom', 'Zoomキー'],
        '252' : ['', '予約済'],
        '253' : ['PA1', 'PA1キー'],
        '254' : ['Clear', 'Clearキー']
    }
}


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    w = Mainwindow(app)
    w.show()
    sys.exit(app.exec())

settings_dialog.py

from PySide6 import QtWidgets, QtCore, QtGui


class SettingsDialog(QtWidgets.QDialog):
    def __init__(self, app, settings) -> None:
        super().__init__()

        self.setWindowTitle('設定')
        self.is_stays_on_top = QtWidgets.QCheckBox('常に最前面に表示')
        self.is_stays_on_top.setChecked(settings['stays_on_top'])

        self.is_frameless = QtWidgets.QCheckBox('ウィンドウフレームを非表示')
        self.is_frameless.setChecked(settings['frameless'])

        self.font_size = QtWidgets.QSpinBox()
        self.font_size.setMaximum(9999)
        self.font_size.setValue(settings['font_size'])

        self.max_row = QtWidgets.QSpinBox()
        self.max_row.setMaximum(9999)
        self.max_row.setValue(settings['max_row'])
        
        self.window_width = QtWidgets.QSpinBox()
        self.window_width.setMaximum(9999)
        self.window_width.setValue(settings['window_width'])

        self.window_height = QtWidgets.QSpinBox()
        self.window_height.setMaximum(9999)
        self.window_height.setValue(settings['window_height'])

        preview_item = QtGui.QStandardItem('Preview text')
        preview_item.setEditable(False)
        preview_item.setSelectable(False)
        self.preview_model = QtGui.QStandardItemModel()
        self.preview_model.appendRow(preview_item)
        self.preview = QtWidgets.QListView()
        self.preview.setMaximumHeight(60)
        self.preview.setModel(self.preview_model)

        self.foreground_color = QtWidgets.QPushButton('文字色:' + settings['foreground_color'])
        self.background_color = QtWidgets.QPushButton('背景色:' + settings['background_color'])

        self.keymap_model = QtGui.QStandardItemModel()
        self.keymap_model.setHorizontalHeaderLabels(['キーコード', '表示名', '説明'])
        self.keymap = MyTableView(app)
        self.keymap.setModel(self.keymap_model)
        self.keymap.verticalHeader().hide()
        self.keymap.horizontalHeader().setStretchLastSection(True)

        for key_code, (name, discription) in settings['keymap'].items():
            item0 = QtGui.QStandardItem()
            item0.setData(int(key_code), QtCore.Qt.ItemDataRole.DisplayRole)
            item1 = QtGui.QStandardItem(name)
            item2 = QtGui.QStandardItem(discription)
            self.keymap_model.appendRow([item0, item1, item2])

        self.mousemap_model = QtGui.QStandardItemModel()
        self.mousemap_model.setHorizontalHeaderLabels(['項目', '表示名'])
        self.mousemap = QtWidgets.QTreeView()
        self.mousemap.setModel(self.mousemap_model)

        buttons = QtGui.QStandardItem('buttons')
        for button, name in settings['mousemap']['buttons'].items():
            buttons.appendRow([QtGui.QStandardItem(button), QtGui.QStandardItem(name)])
            
        actions = QtGui.QStandardItem('actions')
        for action, name in settings['mousemap']['actions'].items():
            actions.appendRow([QtGui.QStandardItem(action), QtGui.QStandardItem(name)])

        root = self.mousemap_model.invisibleRootItem()
        root.appendRow([buttons, QtGui.QStandardItem()])
        root.appendRow([actions, QtGui.QStandardItem()])
        self.mousemap.expandAll()

        button_box = QtWidgets.QDialogButtonBox()
        button_box.addButton('OK', button_box.ButtonRole.AcceptRole).clicked.connect(self.accept)
        button_box.addButton('Cancel', button_box.ButtonRole.RejectRole).clicked.connect(self.reject)

        label0 = QtWidgets.QLabel('フォントサイズ')
        label1 = QtWidgets.QLabel('表示行数')
        label2 = QtWidgets.QLabel('ウィンドウ幅')
        label3 = QtWidgets.QLabel('ウィンドウ高さ')
        label4 = QtWidgets.QLabel('プレビュー')

        self.setLayout(QtWidgets.QGridLayout())
        self.layout().addWidget(self.is_stays_on_top,   0, 0, 1, 2)
        self.layout().addWidget(self.is_frameless,      1, 0, 1, 2)
        self.layout().addWidget(label1,                 2, 0, 1, 1)
        self.layout().addWidget(self.max_row,           2, 1, 1, 1)
        self.layout().addWidget(label2,                 3, 0, 1, 1)
        self.layout().addWidget(self.window_height,     3, 1, 1, 1)
        self.layout().addWidget(label3,                 4, 0, 1, 1)
        self.layout().addWidget(self.window_width,      4, 1, 1, 1)
        self.layout().addWidget(label0,                 5, 0, 1, 1)
        self.layout().addWidget(self.font_size,         5, 1, 1, 1)
        
        self.layout().addWidget(self.foreground_color,  6, 0, 1, 1)
        self.layout().addWidget(self.background_color,  6, 1, 1, 1)

        self.layout().addWidget(label4,                 7, 0, 1, 1)
        self.layout().addWidget(self.preview,           8, 0, 1, 2)
        
        self.layout().addWidget(self.mousemap,          9, 0, 1, 2)
        self.layout().addWidget(self.keymap,            0, 2, 10, 1)
        self.layout().addWidget(button_box,            10, 0, 1, 3)

        self.preview_update()

        self.font_size.valueChanged.connect(self.preview_update)
        self.foreground_color.clicked.connect(self.foreground_change_color)
        self.background_color.clicked.connect(self.background_change_color)

    def foreground_change_color(self):
        def color_changed(color: QtGui.QColor):
            text = self.foreground_color.text()
            self.foreground_color.setText( '{}:{}'.format(text.split(':')[0], color.name()) )
            self.preview_update()
        dialog = QtWidgets.QColorDialog(self)
        dialog.setCurrentColor( self.foreground_color.text().split(':')[1] )
        dialog.currentColorChanged.connect(color_changed)
        dialog.show()

    def background_change_color(self):
        def color_changed(color: QtGui.QColor):
            text = self.background_color.text()
            self.background_color.setText( '{}:{}'.format(text.split(':')[0], color.name()) )
            self.preview_update()
        dialog = QtWidgets.QColorDialog(self)
        dialog.setCurrentColor( self.background_color.text().split(':')[1] )
        dialog.currentColorChanged.connect(color_changed)
        dialog.show()

    def preview_update(self):
        foreground_color = self.foreground_color.text().split(':')[1]
        background_color = self.background_color.text().split(':')[1]
        item = self.preview_model.item(0, 0)
        font = item.font()
        font.setPointSize(self.font_size.value())
        item.setFont(font)
        item.setData(QtGui.QColor(foreground_color), QtCore.Qt.ItemDataRole.ForegroundRole)
        item.setData(QtGui.QColor(background_color), QtCore.Qt.ItemDataRole.BackgroundRole)
        self.preview_model.setItem(0, 0, item)

    def to_dict(self):
        mousemap = { 'buttons' : {}, 'actions' : {} }
        item = self.mousemap_model.invisibleRootItem().child(0, 0)
        for row in range( item.rowCount() ):
            mousemap['buttons'][item.child(row, 0).text()] = item.child(row, 1).text()
        item = self.mousemap_model.invisibleRootItem().child(1, 0)
        for row in range( item.rowCount() ):
            mousemap['actions'][item.child(row, 0).text()] = item.child(row, 1).text()

        keymap = {}
        item = self.keymap_model.invisibleRootItem()
        for row in range( item.rowCount() ):
            keymap[item.child(row, 0).text()] = [ item.child(row, 1).text(), item.child(row, 2).text() ]

        settings = {}
        settings['stays_on_top'] = self.is_stays_on_top.isChecked()
        settings['frameless'] = self.is_frameless.isChecked()
        settings['font_size'] = self.font_size.value()
        settings['max_row'] = self.max_row.value()
        settings['window_width'] = self.window_width.value()
        settings['window_height'] = self.window_height.value()
        settings['foreground_color'] = self.foreground_color.text().split(':')[1]
        settings['background_color'] = self.background_color.text().split(':')[1]
        settings['mousemap'] = mousemap
        settings['keymap'] = keymap

        return settings


class MyTableView(QtWidgets.QTableView):
    
    ctrl_c_pressed = QtCore.Signal()
    ctrl_v_pressed = QtCore.Signal()
    delete_pressed = QtCore.Signal()

    def __init__(self, app) -> None:
        super().__init__()
        self.app: QtWidgets.QApplication = app

    def event(self, event: QtGui.QKeyEvent) -> bool:
        if event.type() == QtCore.QEvent.Type.KeyPress:
            if event.modifiers() == QtCore.Qt.KeyboardModifier.ControlModifier:
                if event.key() == QtCore.Qt.Key.Key_C:
                    self.table_copy()
                    self.ctrl_c_pressed.emit()
                    return True
                if event.key() == QtCore.Qt.Key.Key_V:
                    self.table_paste()
                    self.ctrl_v_pressed.emit()
                    return True
            else:
                if event.key() == QtCore.Qt.Key.Key_Delete:
                    self.table_clear()
                    self.delete_pressed.emit()
                    return True
        return super().event(event)

    def table_copy(self):
        indexes = self.selectedIndexes()
        if len(indexes) < 1:
            return
        data, r = [[]], indexes[0].row()
        for index in indexes:
            if index.row() - r == 1:
                data.append([])
            data[-1].append( index.data() )
            r = index.row()
        text = '\n'.join(['\t'.join([ str(d) for d in row_data ]) for row_data in data])
        clipboard = self.app.clipboard()
        clipboard.setText(text)

    def table_paste(self):
        indexes = self.selectedIndexes()
        if len(indexes) < 1:
            return
        model, r0, c0 = indexes[0].model(), indexes[0].row(), indexes[0].column()
        data = [ row_data.split('\t') for row_data in self.app.clipboard().text().split('\n') ]

        if len(data) == 1 and len(data[0]) == 1:
            for index in indexes:
                if not index.isValid():
                    continue
                model.setData(index, data[0][0])
            return
        
        for r, row_data in enumerate( self.app.clipboard().text().split('\n') ):
            for c, data in enumerate( row_data.split('\t') ):
                index = model.index(r0 + r, c0 + c)
                if not index.isValid():
                    continue
                model.setData(index, data)

    def table_clear(self):
        indexes = self.selectedIndexes()
        if len(indexes) < 1:
            return
        model = indexes[0].model()
        for index in indexes:
            if not index.isValid():
                continue
            model.setData(index, '')


if __name__ == '__main__':
    import sys
    import json
    with open('settings.json', mode='r', encoding='utf-8') as f:
        settings = json.load(f)
    app = QtWidgets.QApplication(sys.argv)
    w = SettingsDialog(app, settings)
    w.show()
    sys.exit(app.exec())

Share post

Related Posts

コメント