1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

HTML形式のクリップボードをPowerShellで取得するときの文字化けの原因を探る

Last updated at Posted at 2025-11-16

はじめに

筆者の下記の記事の続きである。

Windows では HTML 形式のクリップボードデータを UTF8 形式で返すため,Windows.Forms の Clipboard.GetText メソッドを用いると文字化けに苦しんだ。このため Clipboard.GetData メソッドを用いて UTF8 形式でエンコードされたバイト列を取得し,Encoding.UTF8.GetString メソッドを用いて string 型に変換するという技を駆使した。このとき Clipboard.GetData メソッドにはクリップボード形式を文字列として与えるが,仕様の DataFormats.Html に従って "HTML Format" と指定すると文字化けした文字列が返ってくる。ところが "HTML FORMAT" あるいは "html format" のように大文字・小文字を変えた文字列を指定すると System.IO.MemoryStream 型のデータが返ってきて,無事 UTF8 形式のバイト列を取得できた。

これは何故か?という疑問に対する調査記事である。

結論

先に結論をまとめておく。

  • PowerShell から .NET Framework の Clipboard.GetData メソッドを呼び出す際にクリップボードの形式を文字列として与えるとき,この文字列自体は .NET Framework 内部で解釈されるので大文字・小文字は区別される点に注意する必要がある。
  • .NET Framework の Clipboard.GetData メソッドでは,まず Win32 API を呼び出してクリップボードのデータを取得する。このとき所望するデータ形式を文字列で指定し,Windows の標準クリップボード形式名および登録済みのクリップボード形式一覧と比較する。このときの比較は大文字・小文字を区別しない
  • .NET Framework の Clipboard.GetData メソッドでは,得られたデータを .NET Framework のデータ型に変換する。このとき所望するデータ形式は,定義済みのクリップボード形式文字列である DataFormats 列挙体と大文字・小文字を区別して比較され,一致するとデータ型を変換して返す。TextUnicodeTextHTML Format などテキストデータは基本的に string 型に変換して返す。一致しなければ,System.IO.MemoryStream 型で返す。
  • この string 型への変換処理において,初期の .NET Framework 3.5 以前には HTML 形式のクリップボードデータを UTF8 形式として取り扱っていなかった等のバグがあった。また最近の .NET においても末尾の1バイトのコピーが漏れている等のバグがある。ただし,バグがあるのは Windows.Forms で,PresentationCore (WPF) は問題ない。

なお,Clipboard.GetData メソッドの仕様1を読むと,返値は System.IO.MemoryStream 型であるという記載はない。

Returns
Object
An Object representing the Clipboard data or null if the Clipboard does not contain any data that is in the specified format or can be converted to that format.

返値
Object
クリップボードデータを表すオブジェクト,またはクリップボードに指定された形式のデータが存在しない,もしくはその形式に変換できない場合は null

つまり,指定された形式に変換される(場合がある)ことを述べており,指定した形式が TextUnicodeText のようなテキストデータであれば string 型に変換して返すのはむしろ自然に思える。

上記の結論に至るまでの調査結果を以下に示す。

調査その1)API を叩いてみる

まず API を叩いてみた。

(1) DataFormats 列挙体の文字列

Windows.Forms と PresentationCore(WPF) の両方で DataFormats 列挙体が定義されている23が,微妙に異なるので要注意である。

(1-1) ソースコード

長くはないが,退屈なコードなので興味のある方は参照されたい。

表1のデータを生成する C# プログラム(2個)のソースコードはコチラ
data_forms.cs,Windows.Forms版
using System;
using System.IO;
using System.Windows.Forms;
class DATA {
	static	int	Main() {
		Console.WriteLine(DataFormats.Bitmap);
		Console.WriteLine(DataFormats.CommaSeparatedValue);
		Console.WriteLine(DataFormats.Dib);
		Console.WriteLine(DataFormats.Dif);
		Console.WriteLine(DataFormats.EnhancedMetafile);
		Console.WriteLine(DataFormats.FileDrop);
		Console.WriteLine(DataFormats.Html);
		Console.WriteLine(DataFormats.Locale);
//		Console.WriteLine(DataFormats.MetafilePicture);
		Console.WriteLine(DataFormats.MetafilePict);
		Console.WriteLine(DataFormats.OemText);
		Console.WriteLine(DataFormats.Palette);
		Console.WriteLine(DataFormats.PenData);
		Console.WriteLine(DataFormats.Riff);
		Console.WriteLine(DataFormats.Rtf);
		Console.WriteLine(DataFormats.Serializable);
		Console.WriteLine(DataFormats.StringFormat);
		Console.WriteLine(DataFormats.SymbolicLink);
		Console.WriteLine(DataFormats.Text);
		Console.WriteLine(DataFormats.Tiff);
		Console.WriteLine(DataFormats.UnicodeText);
		Console.WriteLine(DataFormats.WaveAudio);
//		Console.WriteLine(DataFormats.Xaml);
//		Console.WriteLine(DataFormats.XamlPackage);
		return 0;
	}
}
data_wpf.cs,PresentationCore(WPF)版
using System;
using System.IO;
using System.Windows;
class DATA {
	static	int	Main() {
		Console.WriteLine(DataFormats.Bitmap);
		Console.WriteLine(DataFormats.CommaSeparatedValue);
		Console.WriteLine(DataFormats.Dib);
		Console.WriteLine(DataFormats.Dif);
		Console.WriteLine(DataFormats.EnhancedMetafile);
		Console.WriteLine(DataFormats.FileDrop);
		Console.WriteLine(DataFormats.Html);
		Console.WriteLine(DataFormats.Locale);
		Console.WriteLine(DataFormats.MetafilePicture);
		Console.WriteLine(DataFormats.OemText);
		Console.WriteLine(DataFormats.Palette);
		Console.WriteLine(DataFormats.PenData);
		Console.WriteLine(DataFormats.Riff);
		Console.WriteLine(DataFormats.Rtf);
		Console.WriteLine(DataFormats.Serializable);
		Console.WriteLine(DataFormats.StringFormat);
		Console.WriteLine(DataFormats.SymbolicLink);
		Console.WriteLine(DataFormats.Text);
		Console.WriteLine(DataFormats.Tiff);
		Console.WriteLine(DataFormats.UnicodeText);
		Console.WriteLine(DataFormats.WaveAudio);
		Console.WriteLine(DataFormats.Xaml);
		Console.WriteLine(DataFormats.XamlPackage);
		return 0;
	}
}

(1-2) ビルド方法

Windows 組み込みの C# コンパイラでビルドが通る。

ビルド方法
csc -o -w:4 data_forms.cs
csc -o -w:4 data_wpf.cs -r:WPF\PresentationCore.dll

(1-3) 実行方法

コマンドプロンプトから引数なしで実行するだけよい。

実行方法
data_forms
data_wpf

(1-4) 結果

C# プログラムを実行した結果である。両者で微妙に異なるフィールドを黄色 #ffffcc で示す。

(2) 標準クリップボード形式一覧

Windows の標準クリップボード形式一覧4を以下に示す。

(3) 登録済みのクリップボード形式一覧

Windows ではシステムによって定義された標準クリップボード形式に加えて,アプリケーション独自のクリップボード形式を登録して使用できる5。これらアプリケーション独自のクリップボード形式を「登録済みのクリップボード形式」と呼ぶ。

(3-1) ソースコード

ソースコードを以下に示す。

list_format.c
#include <windows.h>
#include <stdio.h>
#pragma	comment(lib, "user32.lib")
int		main() {
	char	buf[BUFSIZ];
	for(UINT i = 0xC000; i < 0x10000; i++)
		if(0 != GetClipboardFormatName(i, buf, sizeof(buf)))
			fprintf(stdout, "%04X\t%s\n", i, buf);
	return 0;
}

(3-2) ビルド方法

ビルドには Visual Studio 2022 Community Edition の Visual C++(32bit版)を用いた。

ビルド方法
cl /O2 /W4 list_format.c

(3-3) 実行方法

コマンドプロンプトから引数なしで実行するだけよい。

実行方法
list_format

(3-4) 結果

長くなるのでごく一部のみ抜粋するが,興味のある方は上記のプログラムをコンパイルして確かめて欲しい。筆者の環境では 1000 個以上の形式を出力した。

list_format.exe の出力
...
C081	Rich Text Format
...
C0F3	GIF
C0F4	JFIF
C0F5	PNG
...
C14E	HTML Format
...
C2A6	Csv

ちなみに TextUnicodeText は無かった。Bitmap も無かった。これらは標準クリップボード形式の中に入っているから当然か。HTML Format は標準クリップボード形式にも入っていて重複しているが,おそらく,これは標準クリップボード形式に追加された時期が遅かったせいだろう。

(4).NET Framework のクリップボード形式名とデータ型

(4-1) ソースコード

Windows.Forms 版と PresentationCore(WPF) 版の二つあるが,コードの 99% は共通する。using ディレクティブの行が一行異なるだけであるが,こういうのを上手く一つのプログラムにまとめる方法は無いだろうか?

表3のデータを生成する C# プログラム(2個)のソースコードはコチラ
list_forms.cs,Windows.Forms版
using System;
using System.IO;
using System.Windows.Forms;
class LIST {
	delegate	void	LOCALFUNC(string format);
	[STAThread]
	static	int	Main() {
		var	obj = Clipboard.GetDataObject();
		if(obj == null) return -1;
		LOCALFUNC	GetTypeString = delegate(string format) {
			var	type = "---";
			try {
				if(obj.GetDataPresent(format)) {
					var	data = obj.GetData(format);
					if(data != null)
						type = data.GetType().FullName;
				}
			} catch(Exception) {
				/* 何もしない */
			}
			Console.WriteLine("{0}\t{1}", format, type );
		};
		var	a = obj.GetFormats();
		for(var i = 0; i < a.Length; i++) {
			string	format = a[i];  
			GetTypeString(format.ToUpper());
			GetTypeString(format);
			GetTypeString(format.ToLower());
		}
		return 0;
	}
}
list_wpf.cs,PresentationCore(WPF)版
using System;
using System.IO;
using System.Windows;
class LIST {
	delegate	void	LOCALFUNC(string format);
	[STAThread]
	static	int	Main() {
		var	obj = Clipboard.GetDataObject();
		if(obj == null) return -1;
		LOCALFUNC	GetTypeString = delegate(string format) {
			var	type = "---";
			try {
				if(obj.GetDataPresent(format)) {
					var	data = obj.GetData(format);
					if(data != null)
						type = data.GetType().FullName;
				}
			} catch(Exception) {
				/* 何もしない */
			}
			Console.WriteLine("{0}\t{1}", format, type );
		};
		var	a = obj.GetFormats();
		for(var i = 0; i < a.Length; i++) {
			string	format = a[i];  
			GetTypeString(format.ToUpper());
			GetTypeString(format);
			GetTypeString(format.ToLower());
		}
		return 0;
	}
}

(4-2) ビルド方法

Windows 組み込みの C# コンパイラでビルドが通る。

ビルド方法
csc -o -w:4 list_forms.cs
csc -o -w:4 list_wpf.cs -r:WPF\PresentationCore.dll

(4-3) 実行方法

何らかのデータがクリップボードに入った状態で以下のコマンドを実行する。データは標準出力に出力されるので適当なファイルにリダイレクトする。

実行方法
list_forms > data_forms.txt
list_wpf   > data_wpf.txt

申し訳ないが,このプログラムは不安定でときどき実行に失敗する場合がある。

(4-4) 結果

以下の表は .NET Framework に主要なクリップボード形式名を与えて得られたデータの型を調べたものである。表は3段ずつ同様の形式名を与えているが,中央は正式名,上段はすべて大文字,下段がすべて小文字に変更してデータを取得したもので,空白はデータが得られなかったことを意味する。

背景が黄色 #ffffcc の行は,DataFormats 列挙体の正式名と大文字・小文字を区別して一致していることを示す。

図形データは中央の正式名でしか得られないものが多いが,テキストデータは中央の正式名のみ string 型,上下段は MemoryStream 型で得られるものが多いことに気づく。

調査その2).NET Framework のソースを読む

※表示行数を圧縮するため空行を削除する等の加工を行っている。

.NET Framework 3.5 以前の Windows.Forms

参考文献6より System.Windows.Forms の DataObject.cs を引用。

そもそも どのバージョンの.NET Framework なのか定かではないが,2007年にマイクロソフトより公開されたとのコメントより,.NET Framework 3.5 以前のものと推察される。

Clipboard.GetData メソッドは内部で Clipboard.GetDataObject を呼び出しており,さらに DataObject クラスのメソッドを呼び出していることから,DataObject クラスのソースを探ったところ,下記の GetDataFromHGLOBLAL メソッドが本丸のようである。

まず DataFormats.HtmlDataFormats.Text 等と同類に扱っている。これは明らかにバグに見える。ReadStringFromHandle メソッドの第二引数は Unicode 文字列かどうかを示す真偽値であり,返値は string 型である。また string.Equals メソッドは大文字・小文字を区別するので,大文字・小文字の違いより一致しなかった場合は ReadObjectFromHandle メソッドが呼び出されるが,このメソッドは Stream 型のデータを返す。

System.Windows.Forms.DataObject.cs より GetDataFromHGLOBLAL メソッドを抜粋
//------------------------------------------------------------------------------ 
// <copyright file="DataObject.cs" company="Microsoft">
//     Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
//----------------------------------------------------------------------------- 
namespace System.Windows.Forms {
    /// <include file='doc\DataObject.uex' path='docs/doc[@for="DataObject"]/*' /> 
    /// <devdoc>
    ///    <para>Implements a basic data transfer mechanism.</para> 
    /// </devdoc> 
    [ClassInterface(ClassInterfaceType.None)]
    public class DataObject : IDataObject, IComDataObject {
        /// <include file='doc\DataObject.uex' path='docs/doc[@for="DataObject.OleConverter"]/*' /> 
        /// <devdoc>
        ///     OLE Converter.  This class embodies the nastiness required to convert from our 
        ///     managed types to standard OLE clipboard formats. 
        /// </devdoc>
        private class OleConverter : IDataObject {
            /// <include file='doc\DataObject.uex' path='docs/doc[@for="DataObject.OleConverter.GetDataFromHGLOBLAL"]/*' /> 
            /// <devdoc>
            ///     Retrieves the specified form from the specified hglobal. 
            /// </devdoc> 
            /// <internalonly/>
            private Object GetDataFromHGLOBLAL(string format, IntPtr hglobal) { 
                Object data = null;
                if (hglobal != IntPtr.Zero) {
                    //=---------------------------------------------------------------= 
                    // Convert from OLE to IW objects
                    //=---------------------------------------------------------------= 
                    // Add any new formats here... 
                    if (format.Equals(DataFormats.Text) 
                     || format.Equals(DataFormats.Rtf)
                     || format.Equals(DataFormats.Html)
                     || format.Equals(DataFormats.OemText)) {
                        data = ReadStringFromHandle(hglobal, false); 
                    } else if (format.Equals(DataFormats.UnicodeText)) { 
                        data = ReadStringFromHandle(hglobal, true); 
                    } else if (format.Equals(DataFormats.FileDrop)) { 
                        data = (Object)ReadFileListFromHandle(hglobal);
                    } else if (format.Equals(CF_DEPRECATED_FILENAME)) {
                        data = new string[] { ReadStringFromHandle(hglobal, false) }; 
                    } else if (format.Equals(CF_DEPRECATED_FILENAMEW)) { 
                        data = new string[] { ReadStringFromHandle(hglobal, true) }; 
                    } else { 
                        data = ReadObjectFromHandle(hglobal);
                    } 
                    UnsafeNativeMethods.GlobalFree(new HandleRef(null, hglobal));
                }
                return data;
            } 
        } 
    }
} 
// File provided for Reference Use Only by Microsoft Corporation (c) 2007.

同じく同サイトより,DataFormats.cs を引用。GetFormat メソッドでは与えたクリップボード形式の文字列 format と標準クリップボード形式(表2)を比較しているが,まずは大文字・小文字を区別した厳密な比較を行い,一致するものがなければ次に大文字・小文字を区別しないで比較していることが分かる。

System.Windows.Forms.DataFormats.cs より GetFormat メソッドを抜粋
//------------------------------------------------------------------------------
//     Copyright (c) Microsoft Corporation.  All rights reserved.
//-----------------------------------------------------------------------------
namespace System.Windows.Forms {
    ///    Translates
    ///       between Win Forms text-based
    ///     formats and  32-bit signed integer-based
    ///       clipboard formats. Provides  methods to create new  formats and add 
    ///       them to the Windows Registry.
    public class DataFormats { 
        public static readonly string Text          = "Text"; 
        public static readonly string UnicodeText   = "UnicodeText";
        public static readonly string Dib           = "DeviceIndependentBitmap";
        public static readonly string Bitmap        = "Bitmap";
        public static readonly string EnhancedMetafile   = "EnhancedMetafile"; 
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly")] // Would be a breaking change to rename this
        public static readonly string MetafilePict  = "MetaFilePict"; 
        public static readonly string SymbolicLink  = "SymbolicLink";
        public static readonly string Dif           = "DataInterchangeFormat"; 
        public static readonly string Tiff          = "TaggedImageFileFormat";
        public static readonly string OemText       = "OEMText";
        public static readonly string Palette       = "Palette"; 
        public static readonly string PenData       = "PenData"; 
        public static readonly string Riff          = "RiffAudio";
        public static readonly string WaveAudio     = "WaveAudio";
        public static readonly string FileDrop      = "FileDrop";
        public static readonly string Locale        = "Locale"; 
        public static readonly string Html          = "HTML Format"; 
        public static readonly string Rtf           = "Rich Text Format";
        public static readonly string CommaSeparatedValue = "Csv";
        public static readonly string StringFormat  = typeof(string).FullName;
        public static readonly string Serializable  = Application.WindowsFormsVersion + "PersistentObject"; 
        private static Format[] formatList; 
        private static int formatCount = 0;
        /// Gets a  with the Windows Clipboard numeric ID and name for the specified format.
        public static Format GetFormat(string format) {
            lock(internalSyncObject) { 
                EnsurePredefined();
                // It is much faster to do a case sensitive search here.  So do 
                // the case sensitive compare first, then the expensive one.
                for (int n = 0; n < formatCount; n++) {
                    if (formatList[n].Name.Equals(format))
                        return formatList[n];
                } 
                for (int n = 0; n < formatCount; n++) { 
                    if (String.Equals(formatList[n].Name, format, StringComparison.OrdinalIgnoreCase)) 
                        return formatList[n];
                } 
                // need to add this format string
                int formatId = SafeNativeMethods.RegisterClipboardFormat(format); 
                if (0 == formatId) {
                    throw new Win32Exception(Marshal.GetLastWin32Error(), SR.GetString(SR.RegisterCFFailed)); 
                } 
                EnsureFormatSpace(1);
                formatList[formatCount] = new Format(format, formatId);
                return formatList[formatCount++];
            } 
        }
        /// Gets a  with the Windows Clipboard numeric 
        ///    ID and name for the specified ID.
        public static Format GetFormat(int id) {
            return InternalGetFormat( null, id ); 
        }
        ///     Allows a the new format name to be specified if the requested format is not 
        ///     in the list
        private static Format InternalGetFormat(string strName, int id) { 
            lock(internalSyncObject) {
                EnsurePredefined(); 
                 for (int n = 0; n < formatCount; n++) {
                    if (formatList[n].Id == id) 
                        return formatList[n];
                }
                StringBuilder s = new StringBuilder(128); 
                // This can happen if windows adds a standard format that we don't know about, 
                // so we should play it safe. 
                if (0 == SafeNativeMethods.GetClipboardFormatName(id, s, s.Capacity)) { 
                    s.Length = 0;
                    if (strName == null) {
                        s.Append( "Format" ).Append( id );
                    } else {
                        s.Append( strName ); 
                    }
                }
                EnsureFormatSpace(1);
                formatList[formatCount] = new Format(s.ToString(), id);
                return formatList[formatCount++]; 
            }
        }
        ///     ensures that the Win32 predefined formats are setup in our format list.  This
        ///     is called anytime we need to search the list
        private static void EnsurePredefined() { 
            if (0 == formatCount) {
                formatList = new Format [] { 
                    new Format(UnicodeText,  NativeMethods.CF_UNICODETEXT), 
                    new Format(Text,         NativeMethods.CF_TEXT),
                    new Format(Bitmap,       NativeMethods.CF_BITMAP),
                    new Format(MetafilePict, NativeMethods.CF_METAFILEPICT),
                    new Format(EnhancedMetafile,  NativeMethods.CF_ENHMETAFILE), 
                    new Format(Dif,          NativeMethods.CF_DIF),
                    new Format(Tiff,         NativeMethods.CF_TIFF), 
                    new Format(OemText,      NativeMethods.CF_OEMTEXT), 
                    new Format(Dib,          NativeMethods.CF_DIB),
                    new Format(Palette,      NativeMethods.CF_PALETTE), 
                    new Format(PenData,      NativeMethods.CF_PENDATA),
                    new Format(Riff,         NativeMethods.CF_RIFF),
                    new Format(WaveAudio,    NativeMethods.CF_WAVE),
                    new Format(SymbolicLink, NativeMethods.CF_SYLK), 
                    new Format(FileDrop,     NativeMethods.CF_HDROP),
                    new Format(Locale,       NativeMethods.CF_LOCALE) 
                }; 
                formatCount = formatList.Length; 
            }
        }
    }
}
// File provided for Reference Use Only by Microsoft Corporation (c) 2007.

ちなみに EnsurePredefined メソッドで CF_HTMLformatList に追加するのが漏れているが,登録済みのクリップボード形式の取得 GetClipboardFormatName のほうでカバーできているのだろう・・・

.NET Framework 3.5 以前の PresentationCore (WPF)

同じく同サイトより,System.Windows の DataObject.cs を引用。いわゆる PresentationCore (WPF) のほうである。こちらは DataFormats.Html のときは別処理になっており,バグは修正されたようだ。

System.Windows.DataObject.cs より GetDataFromHGLOBLAL メソッドを抜粋
//---------------------------------------------------------------------------- 
// <copyright file=DataObject.cs company=Microsoft>
//    Copyright (C) Microsoft Corporation.  All rights reserved.
// </copyright> 
// Description: Top-level class for data transfer for drag-drop and clipboard. 
// See spec at http://avalon/uis/Data Transfer clipboard dragdrop/Avalon Data Transfer Object.htm 
// History:
//  04/26/2002 : susiA      Created
//  06/04/2003 : sangilj    Moved to WCP 
//--------------------------------------------------------------------------- 
namespace System.Windows {
    #region DataObject Class
    /// <summary>
    /// Implements a basic data transfer mechanism. 
    /// </summary>
    public sealed class DataObject : IDataObject, IComDataObject { 
        #region OleConverter Class
        /// <summary> 
        /// OLE Converter.  This class embodies the nastiness required to convert from our
        /// managed types to standard OLE clipboard formats. 
        /// </summary>
        private class OleConverter : IDataObject {
            /// <summary> 
            /// Retrieves the specified data type from the specified hglobal.
            /// </summary> 
            /// <SecurityNote> 
            /// Critical - reads data from the clipboard
            /// </SecurityNote> 
            [SecurityCritical]
            private object GetDataFromHGLOBAL(string format, IntPtr hglobal) {
                object data; 
                data = null; 
                if (hglobal != IntPtr.Zero) { 
                    //=---------------------------------------------------------------=
                    // Convert from OLE to IW objects
                    //=----------------------------------------------------------------=
                    // Add any new formats here... 
                    if (IsFormatEqual(format, DataFormats.Html)
                     || IsFormatEqual(format, DataFormats.Xaml)) { 
                        // Read string from handle as UTF8 encoding.
                        // ReadStringFromHandleUtf8 will return Unicode string from UTF8 
                        // encoded handle.
                        data = ReadStringFromHandleUtf8(hglobal);
                    } else if (IsFormatEqual(format, DataFormats.Text) 
                            || IsFormatEqual(format, DataFormats.Rtf)
                            || IsFormatEqual(format, DataFormats.OemText) 
                            || IsFormatEqual(format, DataFormats.CommaSeparatedValue)) {
                        data = ReadStringFromHandle(hglobal, false); 
                    } else if (IsFormatEqual(format, DataFormats.UnicodeText)
                            || IsFormatEqual(format, DataFormats.ApplicationTrust)) { 
                        data = ReadStringFromHandle(hglobal, true);
                    } else if (IsFormatEqual(format, DataFormats.FileDrop)) {
                        SecurityHelper.DemandFilePathDiscoveryWriteRead(); 
                        data = (object)ReadFileListFromHandle(hglobal);
                    } else if (IsFormatEqual(format, DataFormats.FileName)) { 
                        SecurityHelper.DemandFilePathDiscoveryWriteRead();
                        data = new string[] { ReadStringFromHandle(hglobal, false) }; 
                    } else if (IsFormatEqual(format, DataFormats.FileNameW)){ 
                        SecurityHelper.DemandFilePathDiscoveryWriteRead();
                        data = new string[] { ReadStringFromHandle(hglobal, true) };
                    } else if (IsFormatEqual(format, typeof(BitmapSource).FullName)) {
                        data = ReadBitmapSourceFromHandle(hglobal); 
                    } else { 
                        data = ReadObjectFromHandle(hglobal);
                    }
                } 
                return data; 
            }
        }
        #endregion OleConverter Class 
    }
    #endregion DataObject Class
}
// File provided for Reference Use Only by Microsoft Corporation (c) 2007.
// Copyright (c) Microsoft Corporation. All rights reserved.

で,別処理となった HTML 形式のデータ取得は下記の通り。末尾のヌル文字 \0 を探して正確なバイト長を求める処理になっている。末尾にヌル文字 \0 が存在しない場合は末尾までコピーするだけである。

System.Windows.DataObject.cs より ReadStringFromHandleUtf8 メソッドを抜粋
            /// <summary> 
            /// Creates a string from the data stored in handle as UTF8.
            /// </summary>
            /// <SecurityNote>
            /// Critical - reads unmanaged memory directly 
            /// </SecurityNote>
            [SecurityCritical] 
            private unsafe string ReadStringFromHandleUtf8(IntPtr handle) {
                string stringData = null; 
                int utf8ByteSize = NativeMethods.IntPtrToInt32(Win32GlobalSize(new HandleRef(this, handle)));
                IntPtr pointerUtf8 = Win32GlobalLock(new HandleRef(this, handle)); 
                try { 
                    int utf8ByteCount;
                    // GlobalSize can return the size of a memory block that may be 
                    // larger than the size requested when the memory was allocated.
                    // So recount the utf8 byte from looking the null terminator.
                    for (utf8ByteCount = 0; utf8ByteCount < utf8ByteSize; utf8ByteCount++) { 
                        // Read the byte from utf8 encoded pointer until get the null terminator.
                        byte endByte = Marshal.ReadByte((IntPtr)((long)pointerUtf8 + utf8ByteCount));  
                        // Break if endByte is the null terminator.
                        if (endByte == '\0') {
                            break;
                        }
                    }
                    if (utf8ByteCount > 0) {
                        byte[] bytes = new byte[utf8ByteCount]; 
                        // Copy the UTF8 encoded data from memory to the byte array.
                        Marshal.Copy(pointerUtf8, bytes, 0, utf8ByteCount);
                        // Create UTF8Encoding to decode the utf8encoded byte to the string(Unicode). 
                        UTF8Encoding utf8Encoding = new UTF8Encoding();
                        // Get the string from the UTF8 encoding bytes. 
                        stringData = utf8Encoding.GetString(bytes, 0, utf8ByteCount);
                    } 
                } finally {
                    Win32GlobalUnlock(new HandleRef(this, handle));
                }
                return stringData; 
            }

最近の .NET の Windows.Forms

こちらは参考文献7より,最近の .NET の System.Windows.Forms の DataObject.cs を引用。コチラも DataFormats.Html のときは別処理になっており,バグは修正されている。

System.Windows.Forms.DataObject.cs より GetDataFromHGLOBLAL メソッドを抜粋
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace System.Windows.Forms {
    /// <summary>
    ///  Implements a basic data transfer mechanism.
    /// </summary>
    [ClassInterface(ClassInterfaceType.None)]
    public class DataObject : IDataObject, IComDataObject {
        /// <summary>
        ///  OLE Converter.  This class embodies the nastiness required to convert from our
        ///  managed types to standard OLE clipboard formats.
        /// </summary>
        private class OleConverter : IDataObject {
            /// <summary>
            ///  Retrieves the specified form from the specified hglobal.
            /// </summary>
            private object GetDataFromHGLOBAL(string format, IntPtr hglobal) {
                object data = null;
                if (hglobal != IntPtr.Zero) {
                    // Convert from OLE to IW objects
                    if (format.Equals(DataFormats.Text)
                     || format.Equals(DataFormats.Rtf)
                     || format.Equals(DataFormats.OemText)) {
                        data = ReadStringFromHandle(hglobal, false);
                    } else if (format.Equals(DataFormats.Html)) {
                        data = ReadHtmlFromHandle(hglobal);
                    } else if (format.Equals(DataFormats.UnicodeText)) {
                        data = ReadStringFromHandle(hglobal, true);
                    } else if (format.Equals(DataFormats.FileDrop)) {
                        data = ReadFileListFromHandle(hglobal);
                    } else if (format.Equals(CF_DEPRECATED_FILENAME)) {
                        data = new string[] { ReadStringFromHandle(hglobal, false) };
                    } else if (format.Equals(CF_DEPRECATED_FILENAMEW)) {
                        data = new string[] { ReadStringFromHandle(hglobal, true) };
                    } else {
                        data = ReadObjectFromHandle(hglobal, DataObject.RestrictDeserializationToSafeTypes(format));
                    }
                    Kernel32.GlobalFree(hglobal);
                }
                return data;
            }
        }
    }
}

ただし,気になる点として HTML 形式のデータを取得したとき,末尾の一文字が欠けてしまうという不具合があって,もしかすると下記のコードの size - 1 が原因かもしれない。末尾にはヌル文字 \0 が入ることを想定し,これを除くための処理なのかもしれないが,他のテキスト形式のメソッド,たとえば ReadStringFromHandle では見られないからだ。

System.Windows.Forms.DataObject.cs より ReadHtmlFromHandle メソッドを抜粋
            private unsafe string ReadHtmlFromHandle(IntPtr handle) {
                IntPtr ptr = Kernel32.GlobalLock(handle);
                try {
                    int size = Kernel32.GlobalSize(handle);
                    return Encoding.UTF8.GetString((byte*)ptr, size - 1);
                } finally {
                    Kernel32.GlobalUnlock(handle);
                }
            }

ちなみに .NET Framework 3.5 以前の PresentationCore (WPF) のほうでは,わざわざヌル文字 \0 を検索して正確なバイト長を求めていたのと比べると,簡潔だが疑問が残る実装である。

なお,最新の PresentationCore (WPF) のソースは抽象度が一段とアップしてもう追い切れない。

感想

HTML 形式のクリップボードに関して Windows.Forms の Glipboard.GetText メソッドにバグがあることは API を叩くだけで概略見当がついたが,本記事では決定的な証拠を掴むため,さらに .NET Framework のソースコード解読まで踏み込んだ。

とはいっても筆者の解読能力が低いため,完全な証拠を突き止められたとは思っていない。

最後に感想を述べて置く。

.NET Framework のソースコード,とくにクリップボード関連の処理を読むと,100ms間隔で10回リトライしてダメだったらタイムアウト判定するなど想像以上に複雑な処理を行っていて驚いた。おそらく,クリップボードのデータはグローバルメモリ上に置かれていて,アクセスの際にはロック/アンロック処理が必要だからと考える。

とはいえ,Windows.Forms の GetDataFromHGLOBAL メソッドはグローバルメモリをメソッド内で解放しているのに対し,PresentationCore (WPF) のほうは解放処理がないなど,両者は似通っているようで細部が微妙に異なるのは気持ちが悪い。

また HTML 形式のデータの取得の際,Windows.Forms の ReadHtmlFromHandle メソッドはグローバルメモリの末尾に必ずヌル文字 \0 があるという前提で作られているが,PresentationCore (WPF) の ReadStringFromHandleUtf8 はヌル文字 \0 が末尾より手前にあっても OK だし,仮に無くても大丈夫な作りになっている。そもそも同じような処理なのにメソッド名からして異なっている。

だが,おそらく,こういうのは気にしたら負けなのだ。

  1. Clipboard.GetData(string) メソッド (System.Windows.Forms) - microsoft

  2. Winfows.Forms の DataFormats クラス - microsoft

  3. PresentationCore(WPF) の DataFormats クラス - microsoft

  4. 標準クリップボード形式 (Winuser.h) - Win32 apps

  5. クリップボードの形式 - Win32 apps - microsoft

  6. View the source code for .NET classes - dotnetframework.org

  7. System/Windows/Forms/DataObject.cs - gitub

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?