potisanのプログラミングメモ

プログラミング素人です。昔の自分を育ててくれたネット情報に少しでも貢献できるよう、情報を貯めていこうと思っています。Windows環境のC++やC#がメインです。

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ビット符号無し整数)なので当たり前といえば当たり前なのですが、思い込みで数時間悩んでいたので今後の為にメモしておきます。