syghの新フラグメント置き場

プログラミングTipsやコード断片の保管場所です。お絵描きもときどき載せます。

ショートカットファイル(.lnk)をC#で読込・解析・編集・作成

Windows 8.1用にHDDを移行したとき、昔使っていた古いドライブレターをいくつか廃止して統合したので、参考資料画像群*1のしおりとして残しておいたショートカットファイル(*.lnk)のリンク切れが大量に発生しました(リンク先の実体ファイルは別ドライブの別ディレクトリに移動したため)。今日はこいつらをなんとかしてみます。
Windowsのショートカットファイルはバイナリ形式なので、リンク先などの中身を解析・編集するには、情報操作をするためのShell APIを使ったプログラムを書く必要があるのですが、今回はC#での一括処理を書いてみました*2

ShortcutConverter.zip

参照設定に「Windows Script Host Object Model」アセンブリを追加しています。

といっても、ただショートカットファイルのターゲットパスをC#コードで置換するだけでは芸がないし応用も効かないので、

  1. まず古いショートカットファイルの内部情報を中間XMLとして書き出す(Lnk2Xml)
  2. パスの置換・正常化はテキストエディターで中間XMLに対して行なう
  3. 編集後の中間XMLから新たにショートカットファイルを生成する(Xml2Lnk)

という柔軟な多段処理を行なえるようにモジュールを分割します。
以下、処理の抜粋。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MyShellHelpers
{
  public static class ShellShortcutHelper
  {
    public static void XmlToLnk(string strInListXmlFilePath)
    {
      var shell = new IWshRuntimeLibrary.WshShell();

      using (var stream = new System.IO.FileStream(strInListXmlFilePath, System.IO.FileMode.Open, System.IO.FileAccess.Read))
      {
        // LINQ to XML (for XPath) を使って読み出す。

        var xDoc = System.Xml.Linq.XDocument.Load(stream);

        var rootNode = xDoc.Root;

        var query =
          from node in rootNode.Elements("Shortcut")
          select node;

        foreach (var elem in query)
        {
          var nonameData = new
          {
            ShortcutFilePath = elem.Attribute("ShortcutFilePath"),
            TargetPath = elem.Attribute("TargetPath"),
            Arguments = elem.Attribute("Arguments"),
            WorkingDirectory = elem.Attribute("WorkingDirectory"),
            Hotkey = elem.Attribute("Hotkey"),
            WindowStyle = elem.Attribute("WindowStyle"),
            Description = elem.Attribute("Description"),
            IconLocation = elem.Attribute("IconLocation"),
          };

          System.Diagnostics.Debug.Assert(nonameData.ShortcutFilePath != null);
          var objShortcut = shell.CreateShortcut(nonameData.ShortcutFilePath.Value) as IWshRuntimeLibrary.IWshShortcut;

          System.Diagnostics.Debug.Assert(nonameData.TargetPath != null);
          objShortcut.TargetPath = nonameData.TargetPath.Value;

          System.Diagnostics.Debug.Assert(nonameData.Arguments != null);
          objShortcut.Arguments = nonameData.Arguments.Value;

          System.Diagnostics.Debug.Assert(nonameData.WorkingDirectory != null);
          objShortcut.WorkingDirectory = nonameData.WorkingDirectory.Value;

          System.Diagnostics.Debug.Assert(nonameData.Hotkey != null);
          objShortcut.Hotkey = nonameData.Hotkey.Value;

          System.Diagnostics.Debug.Assert(nonameData.WindowStyle != null);
          // 1:通常、3:最大化、7:最小化。
          // http://msdn.microsoft.com/ja-jp/library/cc364498.aspx
          objShortcut.WindowStyle = Int32.Parse(nonameData.WindowStyle.Value);

          System.Diagnostics.Debug.Assert(nonameData.Description != null);
          objShortcut.Description = nonameData.Description.Value;

          System.Diagnostics.Debug.Assert(nonameData.IconLocation != null);
          objShortcut.IconLocation = nonameData.IconLocation.Value;

          // IWshShortcut.Save() メソッドを呼び出すと、設定されたプロパティにしたがってショートカット ファイルを保存する。
          // 逆に言えば Save() を実行するまではストレージに保存されない。
          objShortcut.Save();
        }
      }
    }

    public static void LnkToXml(IEnumerable<string> inShortcutFilePaths, string strOutListXmlFilePath)
    {
      var shell = new IWshRuntimeLibrary.WshShell();

      var xmlSettings = new System.Xml.XmlWriterSettings();
      xmlSettings.Indent = true;
      xmlSettings.IndentChars = "\t";
      xmlSettings.Encoding = new UTF8Encoding();

      using (var stream = new System.IO.FileStream(strOutListXmlFilePath, System.IO.FileMode.Create, System.IO.FileAccess.Write))
      {
        using (var xmlWriter = System.Xml.XmlWriter.Create(stream, xmlSettings))
        {
          // 書き込みにも LINQ to XML を使えるが、今回はオーソドックスな方法を使う。

          xmlWriter.WriteStartDocument();
          xmlWriter.WriteStartElement("Shortcuts");
          foreach (var path in inShortcutFilePaths)
          {
            System.Diagnostics.Debug.Assert(System.IO.File.Exists(path));

            // WshShell.CreateShortcut() で既存のショートカット ファイルを開くこともできる。
            var objShortcut = shell.CreateShortcut(path) as IWshRuntimeLibrary.IWshShortcut;

            xmlWriter.WriteStartElement("Shortcut");

            xmlWriter.WriteStartAttribute("ShortcutFilePath");
            xmlWriter.WriteString(path);
            xmlWriter.WriteEndAttribute();

            // NOTE: 64bit OS 上で 32bit アプリケーションから IWshShortcut を使用すると、
            // もとのショートカット ファイルの [リンク先] に "C:\Program Files\..." と記載されてあっても、
            // TargetPath が "C:\Program Files (x86)\..." に勝手に置き換わってしまう模様。
            // 逆に最初から "C:\Program Files (x86)\..." と記載されているものに関しては、
            // 64bit アプリケーションで実行してもそのままとなる。
            // 結論としては、64bit OS でショートカットを読み出し・作成する場合、必ず 64bit ネイティブ プロセスで処理を実行すること。

            //string strTargetPath = objShortcut.TargetPath;

            xmlWriter.WriteStartAttribute("TargetPath");
            xmlWriter.WriteString(objShortcut.TargetPath);
            xmlWriter.WriteEndAttribute();

            xmlWriter.WriteStartAttribute("Arguments");
            xmlWriter.WriteString(objShortcut.Arguments);
            xmlWriter.WriteEndAttribute();

            xmlWriter.WriteStartAttribute("WorkingDirectory");
            xmlWriter.WriteString(objShortcut.WorkingDirectory);
            xmlWriter.WriteEndAttribute();

            xmlWriter.WriteStartAttribute("Hotkey");
            xmlWriter.WriteString(objShortcut.Hotkey);
            xmlWriter.WriteEndAttribute();

            xmlWriter.WriteStartAttribute("WindowStyle");
            xmlWriter.WriteString(objShortcut.WindowStyle.ToString());
            xmlWriter.WriteEndAttribute();

            xmlWriter.WriteStartAttribute("Description");
            xmlWriter.WriteString(objShortcut.Description);
            xmlWriter.WriteEndAttribute();

            xmlWriter.WriteStartAttribute("IconLocation");
            xmlWriter.WriteString(objShortcut.IconLocation);
            xmlWriter.WriteEndAttribute();

            xmlWriter.WriteEndElement();
          }
          xmlWriter.WriteEndElement();
          xmlWriter.WriteEndDocument();
        }
      }
    }
  }
}

困ったことがあったらすぐに自動化ツールをさらっと書けるのがプログラマーの特権ですね。

XmlToLnk()は匿名型やLINQ to XMLを使うなど、やや技巧的に書いています。
なお、ショートカットファイルを読み込むときの注意点はLnkToXml()内のコメントに記載しています。Visual C#を使うときは、プロジェクト設定で[32 ビットの優先]というチェックボックスを外しておきましょう。

*1:実はいかがわしい画像が大半だということはみんなにはナイショだよ。

*2:VBScriptJScriptVB.NETVC++でもやろうと思えばできると思いますが、自分は現時点で最強のインテリセンスを持つVC#が最も好みです。