コードから MMD に任意のファイルを読み込ませる

MMD に、MMD が対応しているモデル、アクセサリ、モーション、ポーズ、プロジェクトなどを自動的に読ませたいときがあります。ありません。
例えばモーションを自動生成する何かを作ったとして、vmd に書き出さずに直接 MMD に張り付けられると便利だと思いませんか。

自動的にドロップ

まずは既に存在する任意のファイルを読み込ませる方法について。
プロセスの遠隔操作は面倒なものですが、幸い MMD はドラッグアンドドロップでファイルを読み込ませることができるので、 ファイルを自動的にドラッグアンドドロップさせるようにすれば良いわけです。
どうやるかというと、WM_DROPFILES というウィンドウメッセージを使用します。

前提としてターゲットアプリケーション (今回は MMD) のトップレベルウィンドウのハンドルを得ているものとします。

投げるファイルの一覧を用意する

あらかじめ投げたいファイルのフルパスを得ます。一つでも複数でも。
ANSI でも Unicode 表現でも良いのですが、今回は Unicode にします。ヌル文字で終端している必要があります。
そのパス達を連結して、最後にさらにヌル文字を追加したものを用意します。

例えば、

C:\...\foo.pmd
C:\...\bar.vmd
C:\...\battledome.pmx

の三つのパスを渡したい場合、

C:\...\foo.pmd(ヌル文字)C:\...\bar.vmd(ヌル文字)C:\...\battledome.pmx(ヌル文字)(ヌル文字)

のようになります。これのバイト表現をファイル一覧と称することにしましょう。
ところで、ワイド文字って 16 ビットだからヌル文字のバイト表現も 00 00 なんですね。

投げる構造体を作成する

ファイル一覧を得たら、次はそのバイト数 + 20 だけ GlobalAlloc しましょう。
なぜ 20 か。20 というよりは正確に言うと DROPFILES の長さなので sizeof(DROPFILES) です。
確保したら GlobalLock なりなんなりしてその HGLOBALDROPFILES として扱います。

そして構造体の値をセットしていきます。 pFiles には sizeof(DROPFILES) の値、 pt にはドロップ先座標の POINTfNC には非クライアント領域であるかどうか、 fWide には 0 以外をセットします。

pt および fNC はターゲットアプリケーションによって都合が良い値をセットしてください。大抵は (0, 0) などの適当な値で良い気がしますが。

構造体を埋めてもまだ 20 バイト以降空きがありますから、そこにファイル一覧を書き込みます。 書き込み終わったら GlobalUnlock でもしましょう。

投げる

投げるための構造体を用意したら、この構造体の HGLOBALwParam として、相手ウィンドウハンドルに PostMessage します。 SendMessage ではなく PostMessage である必要があります。
すると自動的にドロップが実行されると思います!

使い終わった HGLOBALGlobalFree しておきましょう。たまにエラー出たりするので本当に必要なのかは知りません。

ファイルを作らずに自動的にドロップ

相手アプリケーションによっては使えないかもしれませんが、名前付きパイプを使うことによりファイルを作らずに直接データをドロップできます。 方法は簡単で、渡すファイル名をパイプ名にするだけです。CreateFile は名前付きパイプを開けるわけですから、そのまま開いているものなら読んでくれると思います。
パイプを作成してからドロップし、その後接続を待ってデータを流し込みましょう。
また、相手のバッファサイズが分からないと引っかかって止まったりするので、対策として全データを一括でパイプに流し込みます。

何かあった際に送り側のアプリケーションが止まったりするとアレなので、タイムアウトを設けるなどの対策を取っておくのも良いでしょう。

うちの MMD ポーズ設定 / SetMmdTransformationPlugin プラグインはこの方法であらかじめ生成した vpd や vmd を MMD に投げています。

C# によるサンプル

.NET 3.5 以降なら名前付きパイプが扱えるので楽勝ですね。これは .NET 4 のコードですが。

using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipes;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security;
using System.Text;
using System.Threading;
namespace Linearstar.MikuMikuMoving.SetMmdTransformationPlugin
{
static class MmdDrop
{
[SuppressUnmanagedCodeSecurity, DllImport("user32")]
static extern bool PostMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
const uint WM_DROPFILES = 0x233;
struct DropFiles
{
public uint pFiles;
public int x;
public int y;
[MarshalAs(UnmanagedType.Bool)]
public bool fNC;
[MarshalAs(UnmanagedType.Bool)]
public bool fWide;
}
/// <summary>
/// 指定されたウィンドウにファイルをドロップします。
/// </summary>
/// <param name="hWnd">ドロップ先のウィンドウ ハンドル。</param>
/// <param name="file">ドロップするファイル。</param>
public static void DropFile(IntPtr hWnd, MmdDropFile file)
{
DropFile(hWnd, new[] { file });
}
static void DropFile(IntPtr hWnd, IList<MmdDropFile> files)
{
var names = Encoding.Unicode.GetBytes(string.Join("\0", files.Select(_ => _.FullName).ToArray()) + "\0\0");
var pipes = files.Where(_ => _.IsPipe).Select(_ => new
{
Pipe = new NamedPipeServerStream(_.FileName, PipeDirection.Out, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous),
File = _,
}).ToArray();
var dropFilesSize = Marshal.SizeOf(typeof(DropFiles));
var hGlobal = Marshal.AllocHGlobal(dropFilesSize + names.Length);
var dropFiles = new DropFiles
{
pFiles = (uint)dropFilesSize,
x = 0,
y = 0,
fNC = false,
fWide = true,
};
Marshal.StructureToPtr(dropFiles, hGlobal, true);
Marshal.Copy(names, 0, new IntPtr(hGlobal.ToInt32() + dropFiles.pFiles), names.Length);
PostMessage(hWnd, WM_DROPFILES, hGlobal, IntPtr.Zero);
// Marshal.FreeHGlobal(hGlobal);
foreach (var i in pipes)
using (var handle = new ManualResetEvent(false))
{
var success = false;
i.Pipe.BeginWaitForConnection(ar =>
{
try
{
i.Pipe.EndWaitForConnection(ar);
success = true;
try
{
i.File.Stream.CopyTo(i.Pipe, (int)i.File.Stream.Length);
i.Pipe.WaitForPipeDrain();
}
catch (IOException)
{
}
i.Pipe.Dispose();
i.File.Stream.Dispose();
handle.Set();
}
catch (ObjectDisposedException)
{
}
}, null);
if (i.File.Timeout != -1)
ThreadPool.QueueUserWorkItem(_ =>
{
Thread.Sleep(i.File.Timeout);
if (!success && !i.Pipe.IsConnected)
{
i.Pipe.Dispose();
i.File.Stream.Dispose();
handle.Set();
}
});
handle.WaitOne();
}
}
}
/// <summary>
/// 対象のアプリケーションにドロップするファイルのデータを表します。
/// </summary>
class MmdDropFile
{
/// <summary>
/// ファイル名を取得します。
/// </summary>
public string FileName
{
get;
private set;
}
/// <summary>
/// ファイル名を取得します。これはファイルのフルパスまたは名前付きパイプの名前です。
/// </summary>
public string FullName
{
get
{
return this.IsPipe ? @"\\.\pipe\" + this.FileName : this.FileName;
}
}
/// <summary>
/// 基になるストリームを取得します。
/// </summary>
public Stream Stream
{
get;
private set;
}
/// <summary>
/// このファイルが名前付きパイプを使用して転送されるかどうかを取得します。
/// </summary>
public bool IsPipe
{
get
{
return this.Stream != null;
}
}
/// <summary>
/// 転送のタイムアウトを取得または設定します。
/// </summary>
public int Timeout
{
get;
set;
}
/// <summary>
/// ファイル名を指定して MmdDropFile の新しいインスタンスを初期化します。
/// </summary>
/// <param name="fileName">ドロップするファイル パス。</param>
public MmdDropFile(string fileName)
: this(fileName, null)
{
}
/// <summary>
/// ファイル名および基になるストリームを指定して MmdDropFile の新しいインスタンスを初期化します。
/// </summary>
/// <param name="fileName">ドロップするファイルの任意の名前。</param>
/// <param name="stream">ドロップするデータを提供するストリーム。</param>
public MmdDropFile(string fileName, Stream stream)
{
this.Timeout = -1;
this.FileName = stream == null ? fileName : Path.GetFileName(fileName);
this.Stream = stream;
}
}
}
view raw MmdDrop.cs hosted with ❤ by GitHub

参考にしたページ

この記事は次のページを参考に書かれました: エクスプローラからのコピーについて