potisanのプログラミングメモ

趣味のプログラマーがプログラミング関係で気になったことや調べたことをいつでも忘れられるようにメモするブログです。はてなブログ無料版なので記事の上の方はたぶん広告です。記事中にも広告挿入されるみたいです。

Python 3.4 Windowsクリップボード操作クラス

Python 3.4からAPIをがしがし呼び出してWindowsクリップボードを操作するクラスです。tkinterを使った書き込みが出来なかったので勉強がてら作ってみました。ウィンドウ関係の関数は敢えて外してあります。

大雑把な更新履歴

  • 2020/04/15 RtlCopyMemoryをctypes.memmoveに置き換えました。Windows 10環境ではkernel32.dllにRtlCopyMemoryが見つからずにエラーが発生するためです。
  • 2020/04/15 GlobalLock周りの致命的なミスを修正しました。
  • 2020/04/16 RegisterClipboardFormat->RegisterClipboardFormatW

ソースコード

clipboard.py

from ctypes import *
from enum import IntEnum
from array import array

class Clipboard(object):
    def __init__(self, default_ansi_encoding="sjis"):
        self.default_ansi_encoding = default_ansi_encoding

    def open(self):
        if not windll.user32.OpenClipboard(c_void_p(0)):
            raise WindowsError()
        return

    def close(self):
        if not windll.User32.CloseClipboard():
            raise WindowsError()
        return

    def __enter__(self):
        self.open()
        return self

    def __exit__(self, exception_type, exception_value, traceback):
        self.close()
        return False

    def get_data_handle(self, format):
        GetClipboardData = windll.user32.GetClipboardData
        GetClipboardData.restype = c_void_p
        if isinstance(format, int):
            handle = GetClipboardData(c_uint(format))
        elif isinstance(format, str):
            handle = GetClipboardData(c_wchar_p(format))
        else:
            raise ArgumentError()

        if handle == c_void_p(0) and get_last_error() != 0:
            raise WindowsError()
        return handle

    def get_data_size(self, format):
        handle = self.get_data_handle(format)
        size = windll.kernel32.GlobalSize(c_uint32(handle))
        if handle == c_void_p(0) and get_last_error() != 0:
            raise WindowsError()
        return size.value

    def get_data(self, format):
        handle = self.get_data_handle(format)
        if handle == c_void_p(0) and get_last_error() != 0:
            raise WindowsError()

        kernel32 = windll.kernel32
        GlobalSize = kernel32.GlobalSize
        GlobalSize.restype = c_uint32
        size = GlobalSize(handle)
        if size == 0 and get_last_error() != 0:
            raise WindowsError()
        pointer = kernel32.GlobalLock(c_void_p(handle))
        data = bytearray(size)
        if pointer == c_void_p(0) and get_last_error() != 0:
            raise WindowsError()
        try:
            memmove((c_byte*size).from_buffer(data), pointer, size)
        finally:
            kernel32.GlobalUnlock(handle)
        return data

    def get_ansi_text(self, encoding=None):
        if encoding == None:
            encoding = self.default_ansi_encoding
        data = self.get_data(ClipboardFormats.text.value)
        return data[0:len(data)-1].decode(encoding)

    def get_unicode_text(self):
        data = self.get_data(ClipboardFormats.unicode_text.value)
        return data[0:len(data)-2].decode("utf-16")

    def set_data(self, format, data):
        user32 = windll.user32
        kernel32 = windll.kernel32
        GMEM_MOVEABLE = 0x0002
        GlobalAlloc = kernel32.GlobalAlloc
        GlobalAlloc.restype = c_void_p
        handle = GlobalAlloc(GMEM_MOVEABLE, len(data))
        if handle == c_void_p(0):
            raise WindowsError()
        try:
            GlobalLock = kernel32.GlobalLock
            GlobalLock.restype = c_void_p
            pointer = GlobalLock(handle)
            if pointer == c_void_p(0):
                raise WindowsError()
            try:
                memmove(pointer, data, len(data))
            finally:
                kernel32.GlobalUnlock(handle)
            SetClipboardData = user32.SetClipboardData
            SetClipboardData.restype = c_void_p
            if SetClipboardData(format, handle) == c_void_p(0):
                raise WindowsError()
        except:
            kernel32.GlobalFree(handle)
            raise
        return

    def set_ansi_text(self, data, encoding=None):
        if encoding == None:
            encoding = self.default_ansi_encoding
        if not isinstance(data, str):
            raise ArgumentError()
        buf = (data + "\0").encode(encoding)
        self.set_data(ClipboardFormats.text.value, buf)
        return

    def set_unicode_text(self, data):
        if not isinstance(data, str):
            raise ArgumentError()
        buf = (data + "\0").encode("utf-16")
        self.set_data(ClipboardFormats.unicode_text.value, buf)
        return

    def empty(self):
        user32 = windll.user32
        if not user32.EmptyClipboard():
            raise WindowsError()
        return

    def count_formats(self):
        user32 = windll.user32
        return user32.CountClipboardFormats()

    def register_format(self, format_name):
        user32 = windll.user32
        return user32.RegisterClipboardFormatW(c_wchar_p(format_name))

    def get_formats(self):
        EnumClipboardFormats = windll.user32.EnumClipboardFormats
        fmt = EnumClipboardFormats(0)
        formats = array("i")
        while fmt != 0:
            formats.append(fmt)
            fmt =  EnumClipboardFormats(fmt)
        return formats

    def is_format_available(self, format):
        user32 = windll.user32
        IsClipboardFormatAvailable = user32.IsClipboardFormatAvailable
        IsClipboardFormatAvailable.restype = c_bool
        return IsClipboardFormatAvailable(format_name).value

    def get_format_name(self, format, expand_size=256):
        user32 = windll.user32
        GetClipboardFormatNameW = user32.GetClipboardFormatNameW
        GetClipboardFormatNameW.argtypes = [c_uint32, c_wchar_p, c_int32]
        buffer = create_unicode_buffer(expand_size)
        ret = GetClipboardFormatNameW(format, buffer, len(buffer))
        while ret + 1 == len(buffer):
            if get_last_error() != 0:
                raise WindowsError()
            buffer = create_unicode_buffer(len(buffer) + expand_size)
            ret = GetClipboardFormatNameW(format, buffer, len(buffer))
        return buffer.value

    def get_format_names(self, formats, expand_size=256):
        return [self.get_format_name(format, expand_size) for format in self.get_formats()]

#class Clipboard

class ClipboardFormats(IntEnum):
    text = 1
    bitmap = 2
    metafilepict = 3
    ylk = 4
    dif = 5
    tiff = 6
    oem_text = 7
    dib = 8
    palette = 9
    pen_data = 10
    riff = 11
    wave = 12
    unicode_text = 13
    enhmetafile = 14
    drop_handle = 15
    locale = 16
    dib_v5 = 17
    owner_display = 0x0080
    dsp_text = 0x0081
    dsp_bitmap = 0x0082
    dsp_metafilepict = 0x0083
    dsp_enhmetafile = 0x008f
    private_first = 0x0200
    private_last = 0x02ff
    gdi_object_first = 0x0300
    gdi_object_last = 0x03ff

サンプルコード

文字列を書き込む

from clipboard import Clipboard

if __name__ == "__main__":
    with Clipboard() as clipboard:
        clipboard.empty()
        clipboard.set_ansi_text("あいうえお", "Shift_JIS")

格納されているデータのフォーマットを列挙する

from clipboard import Clipboard

if __name__ == "__main__":
    with Clipboard() as clipboard:
        formats = clipboard.get_formats()
        for format in formats:
            print("\"{0:s}\" ({1:0>4X})".format(
                clipboard.get_format_name(format),
                format))

蛇足

UTF-8UTF-16

Python何かでファイル先頭に文字コードとして指定するのはUTF-8ですが、Windows(XP以降)の内部文字コード(W版関数の扱うエンコード)はUTF-16です。

WCHARやwchar_tの実体がushort(unsigned short、16ビット符号無し整数)なので当たり前といえば当たり前なのですが、思い込みで数時間悩んでいたので今後の為にメモしておきます。