[C#]Excel VBA で jQuery のようにアクセスできるC#ライブラリを作る

Excel VBA で jQuery のようにアクセスできるライブラリを作れるか? | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/3217

なところの続き。と言うか、あのときは Excel VBA 内に閉じてないと使いづらいのでは?と思っていたのですが、Excel VBA から COM ライブラリを呼び出す、という方法でもいいことに気づきました。そこで、C# の相互運用(COMアクセス)を利用して、Excel を jQuery 風に使うライブラリ作成の続きおば。

■用法

こんなコードを書いて実行をすると、

Sub test()
    ' 初期化
    Dim obj As New ExQuery.Query
    obj.SetApplication Excel.Application

    obj.Cell("A1").Text = "最初"
    obj.Cell("A2:B10").Text = "埋める"
    ' 背景色を赤に設定
    obj.Cell("A2").CSS("background-color") = RGB(255, 0, 0)

    ' 文字色と背景色を変える
    Dim v As Long
    v = RGB(0, 0, 255)
    With obj.Cell("A3")
        '.CSS("color") = RGB(255, 255, 255)
        .CSS("color") = "#FF00000"
        .CSS("background-color") = RGB(0, 0, 255)
    End With
End Sub

以下のように Excel のシートを操作できます。

先行きは表を簡単に作るとか、検索を楽にするとかを実装したいですね。

■C#のソースコード

面倒なので全文を晒しておきます。プロジェクトを作る時に、

  • Visual Studio 2010 は管理者モードで起動する
  • Microsoft Excel を参照設定する。
  • プロジェクトのプロパティで「COM 相互運用機能の登録」をチェックする

を忘れずに。管理者モードのほうは、COM をレジストリに登録するの必要です。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Excel = Microsoft.Office.Interop.Excel;
using System.Runtime.InteropServices;

namespace Moonmile.ExQuery
{
	[ClassInterfaceAttribute(ClassInterfaceType.AutoDual)]
	[ComVisible(true)]
	public class Query
	{
		private Excel.Application _app;
		private Excel.Workbook _book;
		private Excel.Worksheet _sheet;
		private Excel.Range _sel;

		///
		/// Initalize Excel.Application object
		///
		public Excel.Application Application
		{
			get { return _app; }
			set
			{
				_app = value;
				_book = _app.ActiveWorkbook;
				_sheet = _app.ActiveSheet;
				_sel = _app.Selection;
			}
		}
		public void SetApplication(Excel.Application app)
		{
			this.Application = app;
		}

		public Excel.Workbook Book
		{
			get { return _book; }
			set
			{
				_book = value;
				_sheet = _app.ActiveSheet;
				_sel = _app.Selection;
			}
		}
		public Excel.Worksheet Sheet
		{
			get { return _sheet; }
			set {
				_sheet = value;
				_sel = _app.Selection;
			}
		}
		public Excel.Range Selection
		{
			get { return _sel; }
			set { _sel = value; }
		}

		///
		/// default constructor
		///
		public Query() { }

		public ExRange Cell( object row1, object col1 = null, object row2 = null, object col2 = null )
		{
			if (row1 == null)
				return new ExRange();

			var s = row1 as string;
			if (s != null)
				return Cell(row1.ToString());
			if (row2 == null)
				return Cell(int.Parse(row1.ToString()), int.Parse(col1.ToString()));

			return Cell(
				int.Parse(row1.ToString()), int.Parse(col1.ToString()),
				int.Parse(row2.ToString()), int.Parse(col2.ToString()));
		}

		///
		/// pattern:
		/// Cell("A1")
		/// Cell("A1:B10")
		/// Cell("#id")
		///
		///
		///
		protected ExRange Cell(string s)
		{
			Excel.Range rg;
			if (s.StartsWith("#"))
				rg = _sheet.get_Range(s.Substring(1));
			else if (s.StartsWith("."))
				rg = _sheet.get_Range(s.Substring(1));
			else
				rg = _sheet.get_Range(s);
			return new ExRange(rg);
		}

		///
		/// pattern:
		/// Cell(1,2)
		///
		///
		///
		///
		protected ExRange Cell(int row, int col)
		{
			if (_sheet == null)
				return new ExRange();
			var rg = _sheet.Cells[row, col];
			return new ExRange(rg);
		}

		///
		/// pattern:
		/// Cell(1,2,3,4)
		///
		///
		///
		///
		///
		///
		protected ExRange Cell(int row1, int col1, int row2, int col2)
		{
			if (_sheet == null)
				return new ExRange();
			var rg = _sheet.Range[
				_sheet.Cells[row1, col1], _sheet.Cells[row2, col2]];
			return new ExRange(rg);
		}
	}

	[ClassInterfaceAttribute(ClassInterfaceType.AutoDual)]
	[ComVisible(true)]
	public class ExRange
	{
		private Excel.Range _range ;
		private CSS _css;
		protected internal ExRange() { }

		public ExRange(Excel.Range rg)
		{
			_range = rg;
			_css = new CSS(_range);
		}

		public string Text
		{
			get { return _range.Value; }
			set { _range.Value = value; }
		}

		public CSS css
		{
			get { return _css; }
		}

	}

	[ClassInterfaceAttribute(ClassInterfaceType.AutoDual)]
	[ComVisible(true)]
	public class CSS
	{
		private Excel.Range _range;

		protected internal CSS() { }
		protected internal CSS(Excel.Range rg)
		{
			_range = rg;
		}

		public string this[string key]
		{
			get
			{
				switch (key.ToLower())
				{
					case "color":
						return _range.Font.Color;
					case "background-color":
						return _range.Interior.Color;
				}
				return "0";
			}
			set
			{
				if (value.StartsWith("#"))
				{
					int r = Convert.ToInt32(value.Substring(1, 2), 16);
					int g = Convert.ToInt32(value.Substring(1, 4), 16);
					int b = Convert.ToInt32(value.Substring(1, 6), 16);
					value = (r + g * 0x100 + b * 0x10000).ToString();
				}
				switch (key.ToLower())
				{
					case "color":
						_range.Font.Color = value;
						break;
					case "background-color":
						_range.Interior.Color = value;
						break;
				}
			}
		}
	}
}

■Excel VBA から使う COM 作成のコツ

いくつか引っ掛かるところを書き下しておきます。

Excel VBA の「integer」は、C# の Int64/short  Int16/short にあたります。16ビットの数値なんですね。なので、COM のメソッドの引数は、short にしておく必要があります。そうしないと、Excel VBA から COM に値を引き渡すときにエラーになります。

Cell メソッドの引数で object を使っていますが、VBA の Variant は、object でしか取れないようです。
最初は、Cell メソッドを多重定義して公開していたのですが、VBA 側で、Cell/Cell_2/Cell_3 と名前を変えられてしまうので、デフォルト値を付けて同じ名前で実行できるように変えました。

Excel VBA のインテリセンスを利用するために [ClassInterfaceAttribute(ClassInterfaceType.AutoDual)] という属性をつけます。実は AutoDual を使うと、COM 側のインターフェースが変わる(メソッドの順番が変わるなど)たびに、VB 側のビルドが必要になるのですが、今回は Excel VBA を対象にするのでこのまま使っています。Excel VBA の場合はインタープリタ的に COM を読み込むので、タイプライブラリを起動時に読み込んでくれるためです。このあたりは、以下のサイトを参考にしてください。

.NETコンポーネントをVB6から使用するための方法
http://www.sev.or.jp/ijupiter/world/dc_interrop/dotnet_com_interrop.html
Visual Basic 6.0 から Visual Basic .NET または Visual Basic 2005 アセンブリを呼び出す方法
http://support.microsoft.com/default.aspx?scid=kb;ja;817248
ClassInterfaceType 列挙体 (System.Runtime.InteropServices)
http://msdn.microsoft.com/ja-jp/library/system.runtime.interopservices.classinterfacetype(v=vs.110).aspx

■今後の予定?

css メソッドをちまちまと実装して、Excel VBA の複雑な UI を楽にアクセスできるようになると良いかも。
あと、C# で書いたので内部的に LINQ が使えますよね。Where/Select メソッド等を適当に公開してやれば、Excel VBA で LINQ を使っている感じに、なるかもしれない。とかとか。

カテゴリー: C#, Excel VBA | 3件のコメント

[C++]ATLで64bit版のCOMを作成する

Twitter / Marupeke_IKD: Windows7下でVisualStudio2005 …
https://twitter.com/Marupeke_IKD/status/245237326995927040

なところで、「Windows7下でVisualStudio2005で64bitなATLをVC++で作って、それを同じくVS2005環境で64bitビルドなVBで使おうとして「参照の追加」をするのだけど」の部分が気になって、一応確認してみたという話を少し。

VB6 自体は、Windows 7 上では動かないので、

  • Windows 7 上の Visual Studio 2005 で 64bit の COM を作成
  • Windows XP(かな?)の VB6 で先の COM を参照

なところでしょう。「VB の参照設定に出てこない」というところは、ファイルを参照させれば良いような気もするのですが、ちょっと実験してみます。

■環境

同じ環境を用意したい…つーのは面倒なので、似た環境を用意します。

  • Windows 7 x64 の Visual Studio 2010 で COM を作る。
  • Windows 7 x64 の Excel x86 を使って試す。

VB6 から参照するのも、Excel VBA から参照するのも似たようなものなので、これでテストします。以前、よくやっていた方法です。

■ATLでCOMを作る

  1. Visual Studio 2010 で「ATLプロジェクト」を選択

  1. プロジェクトに「ATLシンプル オブジェクト」を追加

  1. インターフェースを「デュアル」にするのを忘れずに

  1. クラスビューを使って、適当にメソッドやプロパティを追加(ここでは ICSmpのほうに追加)

IDL ファイルが自動生成されます。

 [
 	object,
 	uuid(A64C2AE7-E5C9-4B95-9C69-B48CB5E97966),
 	dual,
 	nonextensible,
 	pointer_default(unique)
 ]
 interface ICSmp : IDispatch{
 	[propget, id(1)] HRESULT Length([out, retval] SHORT* pVal);
 	[propput, id(1)] HRESULT Length([in] SHORT newVal);
 };
 [
 	uuid(9394A921-EE32-4302-B544-90EC6D538D69),
 	version(1.0),
 ]
 library SampleAtlCom64Lib
 {
 	importlib("stdole2.tlb");
 	[
 		uuid(C8A6D4E6-BFAF-48B4-AE57-F65847F2ACA6)		
 	]
 	coclass CSmp
 	{
 		[default] interface ICSmp;
 	};
 };
  1. プロパティのコードを修正
// CCSmp
STDMETHODIMP CCSmp::get_Length(SHORT* pVal)
{
	*pVal = m_length;
	return S_OK;
}


STDMETHODIMP CCSmp::put_Length(SHORT newVal)
{
	m_length = newVal;
	return S_OK;
}

ひとまず、これをビルドすると「Win32」のほうの 32bit 版の COM が作成できます。

■64bit版のCOMを作る

構成マネージャを使って「新規作成」をします。すると、Win32 の構成をコピーして「x64」の構成ができます。

VC++のライブラリは自動的にx64のほうに切り替えられます(これは、VS2005の頃は手動だったはず)。

これをビルドすると、x64/Debug というフォルダに COM が作成されます。Win32 のほうは、Debug フォルダですね。

■Excel VBA から 64bit COM を参照する

Excel VBA を立ち上げて、「ツール」→「参照設定」を開きます。

参照ボタンを押して、先の x64/Debug にある COM(*.dll)を参照させます。

無事に参照が出来た模様

オブジェクトブラウザでも確認

さっき適当に作った Length というプロパティが見れます。

■Excel VBA で動作確認する。

テストコードを使ってステップ実行してみる。

まぁ、普通に実行ができるわけです。

一応 Excel 2010 を確認しておくと、「ファイル」→「ヘルプ」から 32ビット版なのか 64ビット版なのかが分かります。

■どうして、Marupeke_IKD さんの環境では動かないのか?

という訳で、手元の環境では ATL で x64 の COM を作って 32ビット版の Excel VBA から利用できたわけです。
ただし、VB6 の 64ビット環境については、

Visual Basic 6.0 で 64 ビットの Windows オペレーティング システムの使用方法について
http://support.microsoft.com/kb/894373/ja

に書いてある通り、微妙な動きをするそうなので Win32 に揃えてしまったほうがよさそうですね。
あと、Excel 2010 のヘルプを開いて「64」で検索して、「Microsoft Office の 32 ビット版と 64 ビット版を選択する」の項目を見ると、いまいち Office 64bit の下位互換性が不完全であることが分かります。まぁ、不完全というか「切り捨てた」と言っていいのでしょうが、インストールしている office が 32ビットなのか64ビットなのかを判別しながら、Excel VBA を組むのはやりたくないですよね。という訳で、特に不具合がなければ32ビット版にするのがよいのかも。

このテストで作ったのはシンプルな「ATLシンプル オブジェクト」なので、MFC やら外部 DLL を使っている場合は、もっと注意が必要ですよね。そのあたりでうまく参照設定できないのかもしれません。

カテゴリー: C++ | [C++]ATLで64bit版のCOMを作成する はコメントを受け付けていません

[C#]Stringに正規表現の拡張メソッドを追加してLINQで使う

System::String に正規表現が使えたらいいなぁと思いつつ、書いてみたのがこれです。

■用法

        [TestMethod]
        public void TestMethod1()
        {
            string[] lst = {
                "masuda",
                "tomoaki",
                "yamasaki",
                "yumi" };

            var q = from t in lst
                    where t.IsMatch("y.*i")
                    select t;

            Assert.AreEqual(2, q.Count());
            Assert.AreEqual("yamasaki", q.First());
        }

        [TestMethod]
        public void TestMethod2()
        {
            string[] lst = {
                "masuda",
                "tomoaki",
                "yamasaki",
                "yumi" };

            var q = from t in lst
                    where t.IsMatch("ki$")
                    select new { match = t.Match("..ki$") };

            Assert.AreEqual(2, q.Count());
            Assert.AreEqual("oaki", q.First().match);
        }

単純に、Regex の Match, IsMatch メソッドを String クラスにくっつけただけなのですが、LINQ 内で直接使えるというのがメリットですね。別途、new Regex(…) を使えば、LINQ 内でも書けるのすが、それだと文が長くなってしまうしというパターンです。

■実装

実装はおそろしく簡単で以下な感じ。単純に Match, IsMatch をラップします。

using System.Text.RegularExpressions;

namespace Moonmile.StringRegex
{
    public static class StringRegexExtentions
    {
        public static string Match( this string target, string pattern )
        {
            var rx = new Regex(pattern);
            var mc = rx.Matches(target);
            return (mc.Count == 0) ? "" : mc[0].Value;
        }
        public static bool IsMatch(this string target, string pattern)
        {
            var rx = new Regex(pattern);
            return rx.IsMatch( target );
        }
    }
}

■利用方法

で、最終的には何をしたいのかという、以下な感じで2つのファイルの DIFF を調べたかったのです。
2つのファイルに、ファイル名の羅列があって、それを使って同期をするツールですね。マッチさせたい拡張子が限られているのでそれを内部で指定するという現場の即したツールです。

static void Main(string[] args)
{
#if false
	if (args.Length != 2)
	{
	    Console.WriteLine("usage: difffolders [src] [dest]");
	    return;
	}
	var lsrc = new List();
	var ldst = new List();

	StreamReader sr = File.OpenText(src);
	while (!sr.EndOfStream)
	    lsrc.Add(sr.ReadLine());
	sr.Close();
	sr = File.OpenText(dest);
	while (!sr.EndOfStream)
	    ldst.Add(sr.ReadLine());
	sr.Close();

	var llsrc = new List();
	var lldst = new List();

	string[] exts = {
	                    "\\.cpp", "\\.h", "\\.hpp", "\\.rc", "\\.h", "\\.f90", "\\.fi", "\\.for", "\\.inc", "\\.cmn"
	                    };
	foreach (var ext in exts)
	{
	    string ext1 = ext + "$";
	    llsrc.AddRange(lsrc.Where(n => n.IsMatch(ext1) == true).Select(n => n.Replace("C:\\Develop\\Main-branch-0","")));
	    lldst.AddRange(ldst.Where(n => n.IsMatch(ext1) == true).Select(n => n.Replace("C:\\Develop\\Main-branch-1","")));
	}
	llsrc.RemoveAll(n => n.IsMatch("\\Debug")); ;
	lldst.RemoveAll(n => n.IsMatch("\\Debug"));
	llsrc.Sort();
	lldst.Sort();

	// 差分を計算する
	Debug.Print("src count: {0}", llsrc.Count);
	Debug.Print("dst count: {0}", lldst.Count);

	var onlysrc = llsrc.Where(n => !lldst.Contains(n)).Select(n => n );
	var onlydst = lldst.Where(n => !llsrc.Contains(n)).Select(n => n );
	var andlst = llsrc.Where(n => lldst.Contains(n)).Select(n => n);

	Debug.Print("only/and {0} {1} {2}", onlysrc.Count(), onlydst.Count(), andlst.Count());

	StreamWriter sw = new StreamWriter(File.OpenWrite("out-diff.txt"));
	foreach( var f in onlysrc )
	    sw.WriteLine("- {0}", f );
	foreach (var f in onlydst)
	    sw.WriteLine("+ {0}", f);
	foreach (var f in andlst)
	    sw.WriteLine("m {0}", f);
	sw.Close();
}

で、これを作るうえで困ったのが、.NET の正規表現のまずさ(Regexの実装かな?)ですね。本来ならば、

var q = llsrc.Where( n => n.IsMatch("\\.(cpp|h|hpp|rc|f98|for|fi|inc|cmn)").Select( n => n );

のように一発で書きたいところなのですが、.NET 正規表現では、OR 演算子が無い模様。Group を使うんですかね?

2012/09/18 追記

とか思ったら、

代替構成体
http://msdn.microsoft.com/ja-jp/library/36xybswe(v=vs.100).aspx

に「|」が使えるやんッ!!! ってことで、多分先のコードは書けるハズ。後で書き直しますか。

 

 

 

正規表現クラス
http://msdn.microsoft.com/ja-jp/library/30wbz966(v=vs.80)

仕方がないので array で用意して foreach で廻しています。Perl 互換の正規表現ライブラリがあればそれを使いたいなと。

ちなみに、ファイル名から拡張子を取る Path.GetExtension を使っても良いんですけどね。

	string[] exts = {
            ".cpp", ".h", ".hpp", ".rc", ".h", ".f90", ".fi", ".for", ".inc", ".cmn"
            };

	foreach ( var n in lsrc ) {
		if (exts.Contains(Path.GetExtension(n)))
		{
			llsrc.Add(n.Replace("C:\\Develop\\Main-branch-0", ""));
		}
	}
	foreach ( var n in ldst ) {
		if (exts.Contains(Path.GetExtension(n)))
		{
			lldst.Add(n.Replace("C:\\Develop\\Main-branch-1", ""));
		}
	}
カテゴリー: C# | 3件のコメント

Windows 8 に「スタート」ボタンは要らない、という構造

Welcome to Classic Shell
http://classicshell.sourceforge.net/
dnki.co.jp
http://dnki.co.jp/system/joomla_1_0_xx/joomla_1_0_15JP_Stable/content/view/145/1/

なところで Windows 8 用のスタートボタンをダウンロードできるのですが、実は Windows 8 では「スタートボタンが要らない操作/構造」を実現しているというのを書いておきます。ええと、「Classic Shell」はそれはそれでいいんですけどね。

■最初はコマンドプロンプトだった。

皆さまご存じの通り?最初のMS-DOSはUnixの真似っこで、コマンドラインでした。コマンドラインというのは「キーボードでコマンドを打つ」というのと「コマンドの結果がテキストで表示される」ってのが主なインターフェースです。

image

この時に、各アプリケーションはコマンドという形で、DOS ディレクトリや BIN ディレクトリに置いていたのです。Unix のシェルコマンドと同じで、大抵はひとつの exe ファイルになっていたのです。
ユーザーは、アプリケーションを実行したい場合は「コマンド」を「キーボード」で打ちました。打ち込むキーの位置はプロンプトで決まっていて、必ずテキストの最後に入力されていた訳です。いわゆる、CUI という奴ですね。

まあ、その後 Unix にエスケープシーケンスを使った疑似Windowを作るライブラリもできたわけですが。

■複数のWindowに入力する方式

Windows 3.1 から複数のウィンドウを使ってアプリケーションを使うようになりました。Apple II の真似っこという訳で、マウスを使ってウィンドウを切り替えます。実は、MS-DOS の頃から、ゲームアプリなり、一太郎なりはマウスを使うインターフェースを作っていました。

マウスを使う場合には、右手(あるいは左手)がキーボードから離れます。大抵の人間は腕が3本ではないので(1本の人とか0本の人とかが居るからね)、キーボードの2本、マウスに1本というのは結構大変な作業なわけです。なので、いきおい、

  • 主な作業を、マウスを中心にして扱う(1本の腕を使う)
  • 主な作業を、キーボードを中心にして扱う(2本の腕を使う)

という使い分けが出てきたのですね。Unix の世界では、vi, emacs が「環境」と呼ばれる所以で、マウスを使わずに、全てのアプリをキーボードで操作しようという操作方法です。
最初の Windows 3.1 にはタスクバーがありませんでしたが、Windows 95 あたりから?タスクバーが出てきました。デスクトップの表示も OS/2、Apple などの関係から「実行しているアプリケーション」を表示するのか?、「なんらかの作業領域を示すのか?」と二分されていた時期があります。結局「デスクトップ」は「作業場所」になるわけですが。

この時、アプリケーションは肥大化してきて、複数の exe とダイナミックリンクライブラリ dll と各種の設定ファイルというスタイルになってきました。当時、NeXT Step はアプリケーションフォルダを作って、その配下を不可視にするという「インストール」という方法を提示していたのですが、Windows 3.1 あたりは適当に Windows フォルダあたりにばら撒かれていたわけです。

このばらばらのアプリケーションを(個人がインストールしたものも含む)、ちまちまとエクスプローラーを起動して exe をダブルクリックするか、適当なショートカットをデスクトップに貼りつけておいてアイコンをダブルクリックするか、というスタイルを取っていたわけです。そう、ここでアプリケーションを起動するのは主に「マウス操作」だったのですよ。

そうそう、ファイルマネージャは Mac の真似です。Mac のフォルダにアイコンが自由に配置できる、というスタイルを真似しています。

■「Program Files」とスタートメニュー登場

スタートメニューが登場したのは Windows 95 で、この最大の理由かつ宣伝文句が「ちらかった実行ファイルをひとつのところから起動できる」というところでした。先に書いた通り、あらゆるフォルダに散ってしまった exe ファイルは、いちいちエクスプローラーを開かないと起動ができなかった訳です(賢い人は、ショートカットなりプロンプトから起動できるようにしていましたが)。

この大混乱の状態を解決するのが「スタート」ボタンという訳です。
で、この「スタート」の意味は他にも理由があって、

  • デスクトップには、Excel ファイルやら Word ファイルやらが散っている状態になっていた。
  • 実行ファイルのショートカットもデスクトップに散らばって、混乱していた。

という「デスクトップ」の混乱もその要因です。これは、デスクトップに散らばってしまった「Microsoft 製品」のアプリケーションを「スタート」に押し込めることで、目立つ位置に置くことができ、というメリットがあった訳ですよ。当時「Program Files」という空白付きのフォルダ名はいったいなんなのだ、という混乱もありました。Unix の場合は /user/bin や /user/local/bin に置く習慣があるのに、それでも良かったわけですが。ええ、スラッシュとバックスラッシュのようなビルゲイツの「意地」(あるいは反骨精神)だったような気がします。

スタートメニューからの階層は、ぽちぽちとマウスで追うことになります。Win キーで開いて(当時は Ctrl+ESC )、カーソルキーで探すこともできるのですが、主にマウス操作ですね。

■「プログラムの検索」の登場

さて、無事「スタート」メニューの下にアプリケーションを押し込めていたわけですが、フリーソフトも含めて、あらゆるアプリがこのスタートメニューにアイコンを置くようになりました。色々とつも込むわけで、長く使っているうちに階層構造も深くなって、3段階もあるツリーを辿って行かないとアプリが起動できないという、本末転倒なことが起こってきたわけです。

また、HDD の値段が安くなり、ローカルストレージに大量のファイルを置けるようになってきました。そうすると、写真やら仕様書やら何やらかにやらと、フォルダで整理していたものの、何が何処にあるのか分からなくなっている状態になったわけです。更に云えば、「マイドキュメント」のフォルダを C ドライブに置くようにしたために、空き容量の多い D ドライブは全く使われないという、なんともまぁ「ほむほむ」な現象も起こっていたわけです。このあたりは、Microsoft の技術力…というか「先見の無さ」が露呈されるところです(ええ、わざとやっているのかな?という時もありますが)。

で、写真などのファイルを全文検索するという google のツールが登場しました。google はウェブ上の情報を検索することを目的に使っていたし、Microsoft も安泰と思っていたわけですが、突如ローカルの PC の分野にまで踏み込んで来たわけです。そこで、慌てた Microsoft 社は、ローカルの検索をするための「プログラムの検索」を「標準」という形で追加したわけです。Mac にも Finder がありますが、これも似たような機能です。

さて、プログラムを実行するときに階層構造になっているのはいままでと同じでマウスを使います。しかし「プログラムの検索」をするときはキーボードを使うわけです。実は、Win キーを押したときに「プログラムの検索」にカーソルが置かれるので、続けてキーを打てばアプリを検索することもできるのですが、なかなかそういう使い方をしている人はいませんよね。順序が逆で、アプリをマウスで探した結果、どこにあるか分からないので「プログラムの検索」を使うという順序になっています。
なので、再びキーボード+マウスの3本腕が必要になってきたわけです。

■一方で iPad が発売されて、アイコンで起動する

iPad が発売されたのが二年前ですが、突如としてアプリ起動の概念が変わります。実際は iPhone の継承となるのですが、「スタート画面」あるいは「メニュー画面」というのを複数枚用意して、アイコンを並べるという方法です。一見、デスクトップにアイコンが並んだ混乱状態に見えますが、Apple はひと工夫ほどこしました。

  • たくさんのアイコンがある場合は、複数の画面をスライドさせる。
  • アイコンは固定位置になる。

という2点です。Windows のデスクトップは、1枚しかないのでたくさんのアイコンで溢れてしまうそれだけ混乱してしまいます。マルチデスクトップのフリーツールもあるのですが、結局 Microsoft から提供されることはありませんでした。また、あえて「固定位置にアイコンが整列」されることによって、アイコンが「綺麗に並んでいる」というスタイルに戻しました。

あとは、アプリケーションが動作するときは必ず「全画面で動く」というスタイルに戻しました。これは、Apple の iOS 上「戻さざるを得なかった」という制約なのですが、この「戻さざるを得ない」という制約を、「全画面で動くようにして、アプリを自由に使えるようにしました」というのが、ジョブズらしい発想の転換(というか営業方法)ですね。見習いたいところです。

■Windows 8 のスタートボタンをなくした

さて、やっとこさ Windows 8 の「スタート」ボタンに話が戻りますが、タブレット PC への本格的な参入として、Microsoft はスタートボタンを消してスタート画面(メニュー画面)にしました。あの極めて狭い画面の Windows CE の Phone ですらスタートボタンが付いていた(非常に使いづらかった)ものを消したわけです。ちなみに、MS-Office にも「スタート」ボタンが付いていたのですが、Office 2010 あたりからスタートボタンは無くなっています(苦笑)。

タブレット PC の場合は、iPad と同じように、スタート画面で画面をスライドしながらアイコンを探します。先の記事に書いたようにおそらく「意匠」の関係から iPhone/iPad と似たものにしませんでした。なので、デザイン上は非常にダサいのですが、これで用途は足ります。

アプリケーションはタイルに文字や画像を貼りつけられるのですが、見て分かる通り、どのアイコン/タイルが、何をするアプリなのか分かりませんよね(苦笑)。似たようなタイルを作ると何が何やらわからなくなるという混乱は、これでも発生します。まあ、タブレット PC の場合は、ぽちぽちと指でタップするのであまりストレスにはならないかと。所詮、ブラウジングの範疇ですからね。

が、デスクトップを使う場合=仕事で使う場合には、それらのイライラはできるだけ排除したいものです。デスクトップ(作業場所)とアプリの起動とは、すばやく交互に行き来したいですよね。Windows 7 までは、そのための「スタート」ボタンであり、「プログラムの検索」だったわけです。

で、いくつか Windows 8 で作業をしていくと、アプリ起動をするときのストレスが Windows 7 の時よりも減っていることがわかります。

Windows 7 の場合は、

  1. キーボードからマウスに手を移す。
  2. スタートメニューをマウスでクリックする。
  3. 「すべてのプログラム」をマウスでクリックする。
  4. 目的のプログラムをマウスで探し出す。
  5. アプリが起動したら、マウスで大きさを変える。
  6. キーボードで作業を始める。

という具合です。勿論、「プログラムの検索」を使えば

  1. Win キーを押す
  2. キーワードを入力して、目的のアプリを探し出す。
  3. アプリを実行する。

という手順に減ります。この手順は短くてよいのですが、何故かプログラムの検索結果が出るのが非常に遅いんですよね。と言うのも、アプリの検索もファイルの検索も一括で行ってしまっているために、「アプリを起動したい」のに「目的以外のファイルまで出てくる」まで待たされるというストレスがあるのです。なので、大抵の場合は「すべてのプログラム」からマウスで選択しています。

これが Windows 8 の場合には、「スタート」ボタンがないので、

  1. 「迷わず」 Win キーを押す。
  2. キーワードを入力して、目的のアプリを探し出す。
  3. アプリを実行する。

という「迷い」がない様態になるのです。しかも、全てキーボードを使って実行できますよね(実際は、最大化するためにマウスが必要ですが)。やり方が少ないために、ストレスが少なくなると好例です。これを Microsoft 社が「意図」したかどうかは別ですが、少なくとも「すべてのプログラム」や「プログラムの検索」のストレスは減ります。

実は「プログラムの検索」をアプリに限って結果を出せばいいんですけどね。実際 Windows 8 の検索はアプリに限っているので(笑)。

Windows 8 では、デスクトップとスタート画面の切り替えを Win キーのみで行えます。また、アプリ検索の画面も Win キーを2回押せばデスクトップに戻れます。アプリ検索に対しては、Win+Q とうショートカットも用意されていますが、まあ、Win キーを押した後に、直接キーワードを入力するというのがストレスが無い方法です(覚えるショートカットキーが減りますからね)。

という訳で、Windows 8 のデスクトップには「スタート」ボタンが要らなくて、キーボードだけを使って作業ができるというストレスの少ない環境が実現されつつあるという、主にデスクトップ PC に限った環境ですが、なかなか MS-DOS のコマンドラインに戻った雰囲気で快適です。

カテゴリー: windows 8 | 8件のコメント

[C#] Thumbコントロールを継承したユーザコントロールを作る

[C#] WPFのThumbコントロールを使ってドラッグを実装する | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/3701

どうせなので続きをちょっと書いておきます。

Thumb コントロールを使うと、コントロールのドラッグが簡単にできるようになったのですが、汎用的に使うにしては色々な処理をいれないと駄目なのが難点です。特にドラッグに必要な、

  • DragStarted
  • DragCompleted
  • DragDelta

の3つを実装する必要があるのです。なので、これをユーザーコントロールでくるんでしまいます。ユーザコントロールは UserControl を継承していますが、直接 Thumb コントロールを継承するように変更します。

<Thumb x:Class="SampleWpfDragBitmap.ThumbDrag"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="50" d:DesignWidth="50"
            DragCompleted="mark_DragCompleted"
            DragStarted="mark_DragStarted"
            DragDelta="mark_DragDelta" 
       >
    <Thumb.Template>
        <ControlTemplate TargetType="Thumb">
            <Ellipse Fill="{TemplateBinding Background}" Width="50" Height="50"/>
        </ControlTemplate>
    </Thumb.Template>
</Thumb>

内部のテンプレート記述はそのままです。ルートとなるタグが「Thumb」に変えるのがミソですね。
あと、このコントロールを使う方から背景画像(Background)を指定できるように、TemplateBinding を指定しておきます。これで Thumb コントロールには Background プロパティがないのですが、この ThumbDrag コントロールでは背景を指定できるようになります。

/// <summary>
/// ThumbDrag.xaml の相互作用ロジック
/// </summary>
public partial class ThumbDrag : System.Windows.Controls.Primitives.Thumb
{
	public ThumbDrag()
	{
		InitializeComponent();
	}

	private void mark_DragCompleted(object sender, System.Windows.Controls.Primitives.DragCompletedEventArgs e)
	{
	}
	private void mark_DragStarted(object sender, System.Windows.Controls.Primitives.DragStartedEventArgs e)
	{
	}
	private void mark_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
	{
		Canvas.SetLeft(this, Canvas.GetLeft(this) + e.HorizontalChange);
		Canvas.SetTop(this, Canvas.GetTop(this) + e.VerticalChange);
	}
}

ユーザーコントロールのほうで実装するのは、DragDelta イベントだけです。

<Window x:Class="SampleWpfDragBitmap.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525" xmlns:my="clr-namespace:SampleWpfDragBitmap">
    <Canvas >
        <my:ThumbDrag  Canvas.Left="32" Canvas.Top="36" Height="58" x:Name="thumbDrag1" Width="56" BorderThickness="2" BorderBrush="green" Background="#FFB72323" />
        <Button Canvas.Left="428" Canvas.Top="0" Content="Color" Height="23" Name="button1" Width="75" Click="button1_Click" />
        <Button Canvas.Left="428" Canvas.Top="40" Content="Bitmap" Height="23" Name="button2" Width="75" Click="button2_Click" />
    </Canvas>
</Window>

この ThumbDrag コントロールを使う場合は、上記のように my:ThumbDrag とします。一度コンパイルして、ツールバーからドロップすれば自動で作られます。

ボタンを2つ付けておいて、背景の色を変えるテストと、背景にビットマップを貼りつけるテストをいれています。

public partial class MainWindow : Window
{
	public MainWindow()
	{
		InitializeComponent();
	}

	private void button1_Click(object sender, RoutedEventArgs e)
	{
		this.thumbDrag1.Background = Brushes.Green;
	}

	private void button2_Click(object sender, RoutedEventArgs e)
	{
		var bmp = new BitmapImage(new Uri("images/snap.png",UriKind.Relative));
		var br = new ImageBrush(bmp);
		this.thumbDrag1.Background = br;
	}
}

こんな風に、Background プロパティに Brush を指定すれば良いのです。背景画像の場合には、ImageBrush を作成して設定します。

こうやって、コントロールにしておくと複数コピーするのが簡単なんですよね。これは、XAML のコードを3つコピーしたものです。

さて、これを動的に増やしたり減らしたりする場合はどうするかというと…まじめに VisualTreeHelper を使ってもよいのですが、数がさほど多くないのであれば、あらかじめ XAML 上で固定で追加しおいて見えないようにしておく、というのがコード的に楽ですね。「最大値」は決まってしまいますが、業務アプリの場合はこれで十分かと。

カテゴリー: C#, XAML | [C#] Thumbコントロールを継承したユーザコントロールを作る はコメントを受け付けていません

[C#] WPFのThumbコントロールを使ってドラッグを実装する

WPFでコントロールをドラッグ(1) | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/709
Thumbコントロールでドラッグ | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/698

なところで、WPF のドラッグコントロールを探していたのですが、どうやら Thumb コントロールを使って template で形状を変えるのがよさそうです。ここでは、Thumb.Template を使って形状を変えていますが、まぁ、これを static resource に書いても良いし、それは色々ってところでしょう。

<Window x:Class="SampleWpfDrag.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Canvas>
        <TextBlock Canvas.Left="0" Canvas.Top="0" Height="22" Name="textPos" Width="75" Text="x:0 y:0" />
        <Thumb Canvas.Left="37" Canvas.Top="30" Height="30" Name="mark"  Width="30" Background="LightBlue"
               DragCompleted="mark_DragCompleted"
               DragStarted="mark_DragStarted"
               DragDelta="mark_DragDelta"
               >
            <Thumb.Template>
                <ControlTemplate TargetType="Thumb">
                    <Ellipse Fill="LightBlue" Width="30" Height="30" />
                </ControlTemplate>
            </Thumb.Template>
        </Thumb>
    </Canvas>
</Window>

ドラッグイベント自体は、前の記事に書いた通りで、複数のコントロールを利用する場合には、別途イベントの追加の仕方を考えたほうがよいかなと。ただし、ドラッグイベント自体は基本的に変わらないので、なんらかの形で Thumb コントロールを包んでやるほうが実装的には良い気がします。画面上からはみだすとか、そんな感じの動きとか。

public partial class MainWindow : Window
{
	public MainWindow()
	{
		InitializeComponent();
	}

	private void printPos(UIElement el)
	{
		int x = (int)Canvas.GetLeft(el);
		int y = (int)Canvas.GetTop(el);
		textPos.Text = string.Format("x:{0} y:{1}", x, y);
	}

	/// <summary>
	/// ドラッグ開始
	/// </summary>
	/// <param name="sender"></param>
	/// <param name="e"></param>
	private void mark_DragStarted(object sender,
		System.Windows.Controls.Primitives.DragStartedEventArgs e)
	{
		mark.Background = new SolidColorBrush(Colors.Orange);
	}
	/// <summary>
	/// ドラッグ終了
	/// </summary>
	/// <param name="sender"></param>
	/// <param name="e"></param>
	private void mark_DragCompleted(object sender,
		System.Windows.Controls.Primitives.DragCompletedEventArgs e)
	{
		mark.Background = new SolidColorBrush(Colors.Purple);
	}

	/// <summary>
	/// ドラッグ中
	/// </summary>
	/// <param name="sender"></param>
	/// <param name="e"></param>
	private void mark_DragDelta(object sender,
		System.Windows.Controls.Primitives.DragDeltaEventArgs e)
	{
		printPos(mark);
		Canvas.SetLeft(mark, Canvas.GetLeft(mark) + e.HorizontalChange);
		Canvas.SetTop(mark, Canvas.GetTop(mark) + e.VerticalChange);
	}
}

 

カテゴリー: C#, XAML | [C#] WPFのThumbコントロールを使ってドラッグを実装する はコメントを受け付けていません

[C#] HtmlDom の実力を探る…つーかパフォーマンスが悪いけど

PP-Club メンバの美崎さんのライフハッカーの記事

SimpleStyle100回突破記念~編集部が選ぶ美崎薫氏のアプリ25選 : ライフハッカー[日本版]
http://www.lifehacker.jp/2012/05/120417misakihacks.html
プログラムのソースコードから変数を出力するヘルパー『makeMessagebox』(SimpleStyle第119回) : ライフハッカー[日本版]
http://www.lifehacker.jp/2012/08/12-08-30makemessageboxsimplestyle119_1.html

をなんとか活用できないかと、美崎さん当人に相談したところ「ライフハッカーの目次って用意されていないんですよね~ orz」の返事が orz ああ、目次ないんですね、これ。

一応、当人的には、http://www42.tok2.com/home/papermoon/ なところで、自動生成を試みているらしいのですが、ちょっとこれではアレすぎて全体の見通しが付きません。CSS で適当にレイアウトすればいいんでしょうが、自動生成の精度が悪いらしくというところだそうです。

で、本格的なところはライフラッカーへのクローリングか、google api を使うのがよいのですが、ひとまず、先のページからリンクとタイトルぐらいは抜き出して Excel で整理するところまではやりたいと思う訳ですよ。なので、手順としては、

  1. 該当するページを HTML ファイルで「手動」で保存。
  2. HtmlDom を使って、リンクとタイトルを抽出する。
  3. CSVファイルへ書き出し。
  4. Excel で開いてファンクションを使って適当に整形

「手動」なところが混じっていますが、全体の作業量を考えるとなにも全体を「自動化」する必要がありませんッ!!! ってのが PP-Club の信条でして(つーか、私自身の信条)、リンクとタイトルだけを取り出せばよいわけです。

元の HTML ソース

抽出後のソース

Excel に貼りつけ

とする訳です。
実は元の HTML ソースは、

  • タイトルが重複している
  • 別のリンクも含まれている

な感じで、ちょっとプログラム的にはややこしいのです。そう、トライ&エラーが必要なのですね。
…が、HtmlDom を使うと、これぐらい簡単に書けます。

static void Main(string[] args)
{
	string path = "\\temp\\simplestyle_idx.html";

	var doc = new HtmlDocument().Load(path);
	// Aタグで「SimpleStyle 第??回」を含むものを取得
	var cont = new HtmlNavigator(doc)
		.Where(n => n.TagName == "a" &&
			n.Value.Contains("SimpleStyle 第"))
		.Select(n => new { url = n % "href", text = n.Value })
		.Distinct();

	foreach (var n in cont)
	{
		Console.WriteLine("{0}\t{1}",
			n.url, n.text);
	}
}

まあ、これも一発で書けたわけではなくて何度か書き直している訳ですが、これぐらい短いとちまちまと設計を書いているよりも何かとコードを書いたほうが早い訳です。ええ、当然「設計」が必要な場合もあるので、それの話はまた別のときに。

HtmlDom は github で
https://github.com/moonmile/XmlDom

ちなみに、ライフハッカーの SimpleStyle の目次はもうちょっと考えます。元ソース自体が、第87回までしかないのでちょっと古いので。今は120回位です。

ちなみに、現状の HtmlDom は使い物にならない位遅いので、現在パフォーマンスを調節中です。どうやら、COM -> .NET 変換のところが遅すぎるみたいなので、C++/CLI で書き直せばよいかと模索中。

カテゴリー: C#, PP-Club, XmlDom | 1件のコメント

[C++] レガシーライブラリをクラスで包んで再利用する

手元にあるものがレガシー(遺産)と言う訳ではないのですが、15年程まえの技術で作ってあるので、これに機能を追加するのに苦労しています。
まあ、業務的には「元のコードスタイルに合わせてコーディングする」のが良い訳で、そこそこ手を加える量が少なければそれでも良いのですが、がっつりとコーディングしないといけないとなると、ひと苦労ってことになります。

そうなると、レガシーならばレガシーなりに捨て去ってしまう、というのもひとつの選択肢に上がるわけですが、現実的にはそうも言えません。予算的な問題とか、実現可能性の問題などがありもともとのコードを継承しないと駄目なわけです。COBOL やホストコンピュータの世界では普通に起こっている話で、ぼちぼち10年前のコード自体も辛くなってきていますよね、って話です。そういう分野の「考察」も必要かと思い始めていますが、そのあたりは別の機会に。

さて、既存のデータが「単純な構造体」と「ビット配列」で出来ていたとします。機能追加をするときには、このデータにアクセスしなければいけないのですが、ばらばらに保存されている「単純な構造体」と「ビット配列」はちょっとアクセスが手間です。あらかじめクラス構造になっていればよいのですが、諸々の事情があってそうもいかなかった模様です。

■データアクセスを #define マクロで包む

ひとつの方法としては「#define」マクロを使ってデータアクセスを包みます。

// conbiniance macro function
#define GRID_MAX()		GETNTGRID()
#define GRID_ID(_idx)		CPLOT2.GPTS[_idx][0]
#define GRID_X(_idx)		CPLOT2.GPTS[_idx][1]
#define GRID_Y(_idx)		CPLOT2.GPTS[_idx][2]
#define GRID_Z(_idx)		CPLOT2.GPTS[_idx][3]
#define TO_GRID_INDEX(_gid)	(_gid-1)

#define CQUAD4_MAX()	GETNTELEM()
static bool IS_CQUAD4(long index) {
	long n = index+1;
	int type = GETELETYPE(&n);
	return ( type == 5 || type == 6 );
}
#define CQUAD4_EID(_idx)	CPLOT2.IDCELM[_idx][4]
#define CQUAD4_GID1(_idx)	CPLOT2.IDCELM[_idx][0]
#define CQUAD4_GID2(_idx)	CPLOT2.IDCELM[_idx][1]
#define CQUAD4_GID3(_idx)	CPLOT2.IDCELM[_idx][2]
#define CQUAD4_GID4(_idx)	CPLOT2.IDCELM[_idx][3]
#define CQUAD4_PID(_idx)	CPLOT2.IDCELM[_idx][5]
#define CQUAD4_K(_idx)	CPLOT2.IDCELM[_idx][5]

見ると分かりますが、CPLOT2.GPTS と CPLOT2.IDCELM は単純な配列です。データ自体は Fortran 領域にあるので「配列」でアクセスしないと難しいという当時の理由がありました。これを直接アクセスするのは大変なので、#define マクロを作ってみたわけです。

これは結構定番な方法なのですが、関数アクセス風になってしまうのが難点です。アクセスする配列が増えるたびに、何らかの命名規約を作ってひとつひとつマクロを作らないといけません。作るのも面倒ですし、アクセスの方法も面倒です。

int idx   = 10;
int eid   = CQUAD4_EID(idx);
int gid1  = CQUAD4_GID1(idx);
int gid2  = CQUAD4_GID2(idx);
...

分かり易いと言えば分かり易いのですが、オブジェクト指向的には次のように書きたいのです。

int idx = 10;
int eid = CQUAD4(idx).EID;
int gid1 = CQUAD4(idx).GID(0);
int gid2 = CQUAD4(idx).GID(1);
...

更に参照を使って、こんな風に書きたい訳です。

int idx = 10;
auto &el = CQUAD4(idx);
int eid  = el.EID;
int gid1 = el.GID(0);
int gid2 = el.GID(1);
...

■データアクセスを一時オブジェクトを使って包む

C# の場合は、別に CQUAD4 クラスを作成してひとつひとつ作り込むことになるのですが、C++ の場合はもうちょっと便利な方法があります。
基本的に一時オブジェクト扱いで使うようにしてスコープが外れると自動的に解放される、という前提でクラスを作ります。C# の場合は、using を使うところですね。

///<summary>
/// CPLOT2.GPTS easy access class
///</summary>
class GRID {
protected:
	int _idx;
public:
	GRID(int idx) { _idx = idx; }
public:
	int GID() { return CPLOT2.GPTS[_idx][0]; }
	double X()  { return CPLOT2.GPTS[_idx][1]; }
	double Y()  { return CPLOT2.GPTS[_idx][2]; }
	double Z()  { return CPLOT2.GPTS[_idx][3]; }
	static int Size() { return GETNTGRID(); }
};

///<summary>
/// CPLOT2.IDCELM easy access class
///</summary>
class CQUAD4 {
protected:
	int _idx;
public:
	CQUAD4(int idx) { _idx = idx; }
public:
	int EID() { return CPLOT2.IDCELM[_idx][4]; }
	int GID(int n) { return CPLOT2.IDCELM[_idx][n]; }
	int PID() { return CPLOT2.IDCELM[_idx][5]; }
	int K() { return CPLOT2.IDCELM[_idx][5]; }
	static int Size() { return GETNTELEM(); }
};

それぞれのメソッドは #define マクロの時と変わりません。

int idx = 10;
auto &el = CQUAD4(idx);
int eid  = el.EID();
int gid1 = el.GID(0);
int gid2 = el.GID(1);
...

一見、プロパティのように見えますがメンバアクセスのメソッドです。実は、VC++の場合は独自にプロパティが作れるのですが、今回はやめておきます。プロパティを作りたい場合は、下記な記事で説明してあります。

MS-C++ では __declspec(property()) でプロパティを作れるよ | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/3557

内部的に一時オブジェクトが作られるのでスピードが問題ですが、まぁ、そのあたりは大丈夫みたいです。最近の CPU は早いですから。

カテゴリー: C++ | [C++] レガシーライブラリをクラスで包んで再利用する はコメントを受け付けていません

[C++] HtmlDom のパース部分を C++ で書き直す

HtmlDom は LINQ to HTML を目指していますが、かつ HTML が楽に編集できるように更新系(Update/Delete/Insertなど)のメソッドも準備します。

まあ、内部的には XML に直しているので操作は楽なのですが、なんと HTML のパース部分がちと面倒で。

もともとある System.Forms.HtmlDocument 自体には、Children に相当するコレクションがないので、全 DOM を取ることができないんですよね。

HtmlDocument クラス (System.Windows.Forms)
http://msdn.microsoft.com/ja-jp/library/system.windows.forms.htmldocument(v=vs.110).aspx

トリッキーな作りをすれば、これに沿って LINQ ぐらいは作れるのですが、ちょっと使いづらいということで、独自に HtmlDocument, HtmlNode を作っています。
このとき、HTML 文字列から COM の IHTMLDocument2 を使ってパースするのはこんな感じ。

/// <summary>
/// Loading method
/// HtmlDocument to create a HTML string
/// </summary>
/// <param name="html">HTML string</param>
/// <returns></returns>
public HtmlDocument LoadHtml(string html)
{
	// Creating an object using a mshtml.HTMLDocument
	var doc = new HTMLDocument() as IHTMLDocument2;
	doc.write(new object[] { html });
	Load(doc);
	return this;
}

非常に簡単で、IHTMLDocument2 インターフェースにキャストして、COM の write メソッドを呼び出すだけです。これは HTML DOM の document.write に対応しているので、javascript まで実行されてしまうのですが、まぁ、大丈夫みたいです。何故か、COM で直接呼び出した時は、javascript 実行でエラーになる(画面のUIコントロールを探してエラーになるという不具合のようです)らしいのですが、実は大丈夫です。

	CComPtr<IHTMLDocument2> pDoc;
	HRESULT hr = CoCreateInstance(CLSID_HTMLDocument, NULL, CLSCTX_INPROC_SERVER, IID_IHTMLDocument2, (void**)&pDoc);
	//put the code into SAFEARRAY and write it into document
	SAFEARRAY* psa = SafeArrayCreateVector(VT_VARIANT, 0, 1);
	VARIANT *param;
	hr = SafeArrayAccessData(psa, (LPVOID*)&param);
	param->vt = VT_BSTR;
	param->bstrVal = CComBSTR(strHTMLCode).Copy();
	hr = pDoc->write(psa);
	hr = pDoc->close();
	SafeArrayDestroy(psa);

ATL COM を使っています。どっかのサンプルから取ってきたので、実は SafeArrayCreateVector は必要ないかもしれません。サンプルを動作させるとエラーになっていたのですが「CComBSTR(strHTMLCode).Copy();」のように、一度コピーを取ることで、うまく実行できます。ためしに、自分の twitter サイトから HTML をダウンロードして来てパースするとうまく動きました。twitter サイトは javascript を多用しているので、これが大丈夫ならば大抵のサイトは大丈夫だと思います。

肝心のスピードですが、C# 経由の COM アクセスは何故か属性の配列を取るところが非常に遅くなっています。

	// append attributes
	IHTMLAttributeCollection attrs = node.attributes;
	if (attrs != null)
	{
		foreach (IHTMLDOMAttribute at in attrs)
		{
			if (at.specified)
			{
				string nodeValue = "";
				if (at.nodeValue != null)
					nodeValue = at.nodeValue.ToString();
				nn.Attrs.Add(new HtmlAttr { Key = at.nodeName, Value = nodeValue });
			}
		}
	}

部分的なコードですが、foreach のループのところで、attrs の要素を 150 程度廻るのが問題なようです。実は、IHTMLDocument がパースした後の要素では、何故か属性のコレクションが非常にたくさん用意されているのですよね。おそらく onclick などのフック関数のために用意されていると思うのですが、静的なデータを取りたい場合には、これが不要ですし非常に邪魔です。要素数が2,3しかないので、150程度のループを廻すものだから、耐えきれない位おそくなります。
これは、簡単な HTML の場合には問題がなくて、twitter の HTML のように大量な HTML の場合に発覚した現象です。これでは実用に耐えません。

なので、じゃあ、COM アクセス自体を高速化するためにひとまず、C++ で書いてみたのが以下です。

list<XAttr*> *getAttrs( CComQIPtr<IHTMLDOMNode> node )
{
	auto *xattrs = new list<XAttr*>();

	CComPtr<IDispatch> disp;
	node->get_attributes( &disp );
	CComQIPtr<IHTMLAttributeCollection> attrs = disp;
	if ( attrs ) {
		long length = 0;
		attrs->get_length( &length );
		CComPtr<IDispatch> dispa;
		for ( int i=0; i<length; i++ ) {
			CComVariant vt(i);
			attrs->item( &vt, &dispa );
			CComQIPtr<IHTMLDOMAttribute> attr = dispa ;
			if ( attr ) {
				VARIANT_BOOL vtb;
				attr->get_specified( &vtb );
				if ( vtb ) {
					CComBSTR key;
					attr->get_nodeName( &key );
					CComVariant value;
					attr->get_nodeValue( &value );

					xattrs->push_back(new XAttr( CString(key), CString(value.bstrVal)));
				}
			}
			attr.Release();
			dispa.Release();
		}
	}
	attrs.Release();
	disp.Release();
	return xattrs;
}

ループ変数となる length の値は 150 程度なので同じぐらいループが廻っていますが、非常に高速に動きます。多分、CComBSTR か CComVariant と .NET との相互変換の部分で遅くなっている感じがします。これは後で実測してみるつもりです。

という訳で、IE で使っている IHTMLDocument2 を直接使って自前で DOM を作ることができました。
ですが、このパース部分は C++ となっているので、C# の HtmlNode オブジェクトにしないと LINQ が使えないですよね。

って訳で、C++/CLI の出番なんですよ。ええ、VS2010 では C++/CLI のインテリセンスが効かないので、VS2012 で書いたものを VS2010 に戻しますってな感じです。本当は 2008 が良いんですが、間違ってアンインストールしちゃったんですよね。なので、仕方が無く別マシンの VS2012 を借りるという罰ゲームに(VS2010 に VS2012 を入れると MSTest が正常に動かないんですが、これは RTM でなおっているんでしょうか?)

 

カテゴリー: C++ | 1件のコメント

[C#] 何故 enum に拘りたくなるのか?

enum 値に任意の名称やその他の情報を保持する方法について | Masa’s Lab
http://blog.masa1115.com/?p=1062
どうして enum に拘ってるのか… | Masa’s Lab
http://blog.masa1115.com/?p=1078

のところをざっと見て、私なりに考えると、

使ってはいけない定数定義の一例 | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/2589

でも書いていて…ないか、もっと別なところかもしれませんが、基本はC言語の「#define」と「enum」の違いからです。

■#define は数値、enum は型

#define WEEKDAY_SUN 0
#define WEEKDAY_MON 1
#define WEEKDAY_TUE 2
...

enum WEEKDAY {
	SUN = 0,
	MON,
	TUE,
...
};

の違いは、#define のほうは単なる置き換えなので数値あるいは文字列として扱うのですが、enum は「enum WEEKDAY」という型になります。なので、コンパイルするときに、enum WEEKDAY 型にしないとエラーになるという便利な(つーか大抵の場合は面倒くさい)ものがあります。まあ、面倒なので、int 型として比較してしまいますがね。

C# の場合は、これを継承して

public enum WEEKDAY {
	SUN = 0,
	MON,
	TUE,
}

と書けます。当時 Java で書こうとすると

class WEEKDAY
{
	public static final int SUN = 0;
	public static final int MON = 1;
	public static final int TUE = 2;
}

あたりでげんなりしてまったのですが…まあ、最近のjava2 では enum があるようです。げんなりしていたのは、2000年前後ですね。

そうそう、C# の時になぜ「文字列」や別の型を許さなかったのか?と疑問なのですが、よくわかりませんね。string が使えると便利なのに。

public enum WEEKDAY {
	SUN = "Sunday",
	MON = "Monday",
	TUE = "Tuesday",
}

リフレクションがあるから大丈夫?ってことなんでしょうか?

■enumを使わずに実装する

「WeekDay.SUN」という値に、数値と英単語と日本語の3つを割り当てようとすると、なかなか大変なので、大抵は下記のようになります。

/// <summary>
/// 列挙がいらない場合
/// </summary>
public class WeekDay 
{
	// プロパティ定義
	public int Value { get; set; }
	public string Name { get; set; }
	public string NameJa { get; set; }

	// enum の代わり
	static public WeekDay SUN { get { return new WeekDay(0, "Sunday", "日曜日"); }}
	static public WeekDay MON { get { return new WeekDay(1, "Monday", "月曜日"); }}
	static public WeekDay TUE { get { return new WeekDay(2, "Tuesday", "火曜日");}}
	static public WeekDay WED { get { return new WeekDay(3, "Wednesday", "水曜日"); } }
	static public WeekDay THU { get { return new WeekDay(4, "Thursday","木曜日");}}
	static public WeekDay FRI { get { return new WeekDay(5, "Friday", "金曜日"); }}
	static public WeekDay SAT { get { return new WeekDay(6, "Satday", "土曜日"); }}

	protected WeekDay(int value, string name, string nameJa)
	{
		this.Value = value;
		this.Name = name;
		this.NameJa = nameJa;
	}
}

/// <summary>
/// 列挙が必要な場合はリストを作る
/// </summary>
public class WeekDayList : List<WeekDay>
{
	protected WeekDayList() { }

	static public WeekDayList Create()
	{
		var lst = new WeekDayList();
			
		lst.Add(WeekDay.SUN);
		lst.Add(WeekDay.MON);
		lst.Add(WeekDay.TUE);
		lst.Add(WeekDay.WED);
		lst.Add(WeekDay.THU);
		lst.Add(WeekDay.FRI);
		lst.Add(WeekDay.SAT);
		return lst;
	}
	public static WeekDayList List
	{
		get { return WeekDayList.Create(); }
	}
}

foreach が使えるように WeekDayList を定義するかどうかは別なのですが、WeekDay クラスみたいなのをだらだらと作っていく訳です。
0,1,2 といったマジックナンバーを記述するのが嫌なのですが、これはデータベース上にあるマスターデータの定数などが入るため、enum を使っても似たような感じになるでしょう。

で、テストコードがこんな感じ。

	/// <summary>
	/// UnitTest1 の概要の説明
	/// </summary>
	[TestClass]
	public class UnitTest1
	{
		[TestMethod]
		public void TestNormal()
		{
			Assert.AreEqual(0, WeekDay.SUN.Value);
			Assert.AreEqual("Sunday", WeekDay.SUN.Name);
			Assert.AreEqual("日曜日", WeekDay.SUN.NameJa);
		}

		[TestMethod]
		public void TestNormalList()
		{
			var days = WeekDayList.Create();
			Assert.AreEqual(7, days.Count);
			// 最初の要素
			var day = days.GetEnumerator();
			day.MoveNext();
			Assert.AreEqual("日曜日", day.Current.NameJa);
		}

		[TestMethod]
		public void TestNormalList2()
		{
			// foreach で列挙
			string s = "";
			foreach (var d in WeekDayList.List)
			{
				s += string.Format("{0}", d.NameJa[0]);
			}
			Assert.AreEqual("日月火水木金土", s);
		}
	}

プロパティで扱えるのと、foreach で取り出せるほうがよいかと。

難点を云えば「enum の代わり」でしかないので、諸々の機能は実装していく必要があります。こんなのが 2,30あると面倒臭くてやっていけないので、Excel VBA を使って自動生成すると楽です。ちなみに、以前の仕事では、Excel VBA を使ってデータベース仕様書から 1万行 の型付Listとenumを自動生成させました。

■enum を使って実装する

Masa さんのエントリーに準じて、enum に属性を付ける方法をやってみましょう。
enum に属性を付けて、適当な文字列をあらかじめ設定しておくというのは良くやる手段なのですが、どうやって取り出すのかが不明ですよね。確かに、以前、私も考えていたもののうまい方法がなくて断念していました。

で、先のエントリーで enum の拡張メソッドを使えばなんとかなる、ってことが分かったので以下ように実装します。

/// <summary>
/// Enum で実装する場合
/// </summary>
public enum WeekDayEnum
{
	[WeekDay("Sunday","日曜日")]	SUN  = 0,
	[WeekDay("Monday","月曜日")]	MON,
	[WeekDay("Tuesday","火曜日")]	TUE,
	[WeekDay("Wednesday","水曜日")]	WED,
	[WeekDay("Thursday","木曜日")]	THU,
	[WeekDay("Friday","金曜日")]	FRI,
	[WeekDay("Satday","土曜日")]	SAT,
}

/// <summary>
/// 属性設定
/// </summary>
public class WeekDayAttribute : Attribute
{
	internal protected static List<WeekDayAttribute> _lst = null;

	public string Name { get; set; }
	public string NameJa { get; set; }
	public WeekDayAttribute(string name, string nameJa)
	{
		this.Name = name;
		this.NameJa = nameJa;
	}
}

public static class WeekDayEnumExtentions
{
	public static int Value(this WeekDayEnum self)
	{
		return (int)self;
	}
	public static WeekDayAttribute GetCustomAttribute(WeekDayEnum self)
	{
		Type t = typeof(WeekDayEnum);
		var mi = t.GetMember(self.ToString())[0];
		return Attribute.GetCustomAttribute(mi, typeof(WeekDayAttribute)) as WeekDayAttribute;
	}
	public static string Name(this WeekDayEnum self)
	{
		return WeekDayEnumExtentions.GetCustomAttribute(self).Name;
	}
	public static string NameJa(this WeekDayEnum self)
	{
		return WeekDayEnumExtentions.GetCustomAttribute(self).NameJa;
	}
}

カスタム属性 WeekDayAttribute を作るのと、リフレクションを利用した WeekDayEnumExtentions による拡張メソッドです。
リフレクションのあたりのエラー処理をさぼっていますが、まぁ結構すんなりと書けたかと。これだけ短いと、修正するのも楽でしょう、多分。

で、テストコードはこんな感じ。

	[TestClass]
	public class UnitTest2
	{
		[TestMethod]
		public void TestNormal()
		{
			WeekDayEnum day = WeekDayEnum.MON;

			Assert.AreEqual(1,(int)day );
			Assert.AreEqual("Monday",day.Name());
			Assert.AreEqual("月曜日", day.NameJa());
		}

		[TestMethod]
		public void TestNormal2()
		{
			Assert.AreEqual("Monday", WeekDayEnum.MON.Name());
			Assert.AreEqual("月曜日", WeekDayEnum.MON.NameJa());
		}

		[TestMethod]
		public void TestNormalList2()
		{
			// foreach で列挙
			string s = "";
			foreach (WeekDayEnum d in Enum.GetValues(typeof(WeekDayEnum)))
			{
				s += string.Format("{0}", d.NameJa()[0]);
			}
			Assert.AreEqual("日月火水木金土", s);
		}
	}

基本的に一緒なんですが、「拡張メソッド」なので「Name()」や「NameJa()」のように括弧をつけないといけないのいまいちですね。ってのは、Masa さんに同感。

カテゴリー: C# | [C#] 何故 enum に拘りたくなるのか? はコメントを受け付けていません