SplitContainerの自動生成コード不具合

※この現象はVisualStudio2005(.NET Framework 2.0),2008(.NET Framework 3.5)で発生、2010(.NET Framework 4.0)で発生しないことを確認しました。

SplitContainer(パネルの中身を2分割して境界線を自由に移動できるやつです)でデザイナの不具合を発見したので、その回避方法を記述します。

続きを読む

HTMLのダウンロードとパーズ

HTMLのダウンロード

左の図のようなフォームを作成し、ボタンクリックのイベントハンドラに下記のコードを追加すると、textBox1に設定されたURLからファイルをダウンロードし、内容をtextBox2に表示します。

// httpでHTMLを取得
HttpWebRequest webRequest =
                    (HttpWebRequest)WebRequest.Create(textBox1.Text);
HttpWebResponse webResponse =
                    (HttpWebResponse)webRequest.GetResponse();
StreamReader streamReader = new StreamReader(
                    webResponse.GetResponseStream(), Encoding.UTF8);
try
{
    this.textBox2.Text = streamReader.ReadToEnd();
}
catch
{
    MessageBox.Show("例外が発生しました。");
}
streamReader.Close();

HTMLのパーズ

.NET FrameworkにはHTMLをパーズする機能は備わっていないため、Majestic-12C# HTML parser (.NET)を使用します。
ボタンのイベントハンドラに、以下のコードを追加すると、読み込んだHTMLを解析し、titleタグの内容をtextBox3に表示します。

HTMLparser parser = new HTMLparser();
parser.Init(textBox2.Text);
HTMLchunk chunk = null;
bool isTitle = false;
while ((chunk = parser.ParseNext()) != null)
{
    if (chunk.oType == HTMLchunkType.OpenTag)
    {
        if (chunk.sTag.Length == 5 && chunk.sTag == "title")
        {
            if (!isTitle)
            {
                isTitle = true;
            }
        }
    }
    else if (chunk.oType == HTMLchunkType.CloseTag)
    {
        if (chunk.sTag.Length == 5 && chunk.sTag == "title")
        {
            if (isTitle)
            {
                isTitle = false;
                break;
            }
        }
    }
    else if (chunk.oType == HTMLchunkType.Text)
    {
        if (isTitle)
        {
            this.textBox3.Text = chunk.oHTML;
        }
    }
}

サブクラス化で機能拡張を行う

動機

Excel上で、セルの選択範囲が変化した際には、SheetSelectionChangeイベントが発生し、以下のようなハンドラを設定していれば、セルの選択範囲を認識することができます。

private void Application_SheetSelectionChange(object Sh, Excel.Range Target)
{
    this.Label.Text = "";
    foreach (Excel.Range current in Target)
    {
        this.Label.Text +=
            "[" + current.Row.ToString() + ", " + current.Column.ToString() + "]\n";
    }
}

しかし、セル→画像、画像→セルのように、異なるオブジェクトを選択した場合には、固有のイベントが発生しないため、選択されたオブジェクトを認識するには一工夫必要になります。
ここでは、既存のクラスを、機能拡張したサブクラスで乗っ取り、新しいイベントを定義することによって、上記の問題を解決します。

既存クラスの調査

まず、実際にメッセージを処理している部分を特定するために、Spy++による解析を行います。
図中にある照準をドラッグし、解析する範囲を選択します。ここでは、シート上のイベントが知りたいので、シートの外枠を選択します。OKクリック後のダイアログで同期をクリックすると、ウインドウの階層構造の中から、該当するウインドウが選択されます。
選択されたウインドウ名を右クリックし、メッセージを選択すると、新しいウインドウが開きます。ここには、該当するウインドウ上で発生したメッセージが表示されます。
全てのメッセージを表示すると、WM_MOUSEMOVEなどで埋まってしまうため、取り除いていきます。
これにより、セル選択時にはWM_CAPTURECHANGED、画像選択時にはWM_PAINT、ウインドウ切り替え時にはWM_SETFOCUSが発生していることが分かります。

サブクラスの定義

既存のウインドウをサブクラス化する際には、NativeWindowクラスを継承した新しいクラスを作成し、WndProcメソッドをオーバーライドすることで、既存のメッセージループに処理を追加することが可能です。以下に実装を示します。

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

namespace ExcelAddinWithNativeWindow
{
    public class NativeExcelControl : NativeWindow, IDisposable
    {
        public event EventHandler<EventArgs> SelectChanged = null;

        public NativeExcelControl(IntPtr hTarget)
        {
            if (hTarget == IntPtr.Zero)
            {
                throw new System.ApplicationException();
            }
            else
            {
                this.AssignHandle(hTarget);
            }
        }

        ~NativeExcelControl()
        {
            this.Dispose();
        }

        public void Dispose()
        {
            this.ReleaseHandle();
        }

        protected override void WndProc(ref Message m)
        {
            const int WM_SETFOCUS = 0x0007;
            const int WM_PAINT = 0x000F;
            const int WM_CAPTURECHANGED = 0x0215;
            if (m.Msg == WM_CAPTURECHANGED || m.Msg == WM_PAINT || m.Msg == WM_SETFOCUS)
            {
                if (Globals.ThisAddIn.Application.Selection != prevSelected)
                {
                    this.OnSelectChanged();
                }
            }

            base.WndProc(ref m);
        }

        protected virtual void OnSelectChanged()
        {
            if (this.SelectChanged != null)
            {
                prevSelected = Globals.ThisAddIn.Application.Selection;
                this.SelectChanged(this, EventArgs.Empty);
            }
        }

        private object prevSelected = null;
    }
}

this.AssignHandle(hTarget)で元のウインドウを乗っ取り、this.ReleaseHandle()で解放するようになっています。
WM_CAPTURECHANGED、WM_PAINT、WM_SETFOCUSの3つを横取りしていますが、これらのイベントが発生しても、選択したオブジェクトに変更がない場合がありますので、前回の値を記憶し、異なっている場合にのみ、イベントを発生させます。

ウインドウの乗っ取り

実際にウインドウを乗っ取るには、別のコードが必要になります。まず、実際に乗っ取る対象を探すコードを示します。

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

namespace ExcelAddinWithNativeWindow
{
    static class NativeExcelControlHelper
    {
        // Win32 APIのインポート
        [DllImport("USER32.dll")]
        public static extern IntPtr FindWindowEx(
            IntPtr hWndParent, IntPtr hWndChildAfter,
            string strClass, string strWindows);

        static public List<IntPtr> FindTarget()
        {
            List<IntPtr> result = new List<IntPtr>();
            IntPtr hExcelDesk = IntPtr.Zero;
            IntPtr hExcelPrev = IntPtr.Zero;
            IntPtr hExcelBook = IntPtr.Zero;
            hExcelDesk = FindWindowEx((IntPtr)Globals.ThisAddIn.Application.Hwnd,
                IntPtr.Zero, "XLDESK", null);
            if (hExcelDesk != IntPtr.Zero)
            {
                while (true)
                {
                    hExcelBook = FindWindowEx(hExcelDesk, hExcelPrev, "EXCEL7", null);
                    if (hExcelBook != IntPtr.Zero)
                    {
                        result.Add(hExcelBook);
                        hExcelPrev = hExcelBook;
                    }
                    else
                    {
                        break;
                    }
                }
            }
            return result;
        }
    }
}

XLDESKというクラスの下にある、EXCEL7というクラスをすべて列挙し、リストで返しています。このEXCEL7というクラスが乗っ取りの対象となります(Spy++の図の内容を思い出してください)。

ウインドウの乗っ取り2

次に、実際にウインドウの乗っ取りを行っているコードを示します。

private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
    this.Application.WorkbookActivate +=
        new Excel.AppEvents_WorkbookActivateEventHandler(
            Application_WorkbookActivate);
    NativeExcelControl ctrl = new NativeExcelControl(
        NativeExcelControlHelper.FindTarget()[0]);
    ctrl.SelectChanged += new EventHandler<EventArgs>(
        this.NativeExcelControl_SelectChanged);
    ecs.Add(ctrl);

    Microsoft.Office.Tools.CustomTaskPane customTaskPane =
        this.CustomTaskPanes.Add(usercontrol1, "アクションペイン");
    customTaskPane.DockPosition = 
        Microsoft.Office.Core.MsoCTPDockPosition.msoCTPDockPositionRight;
    customTaskPane.Visible = true;
    this.Label.Text = "";
    this.Label2.Text = "";
}

最初の3行は後回しにして、実際にNativeExcelControlクラスが使用されている部分に着目します。まず、乗っ取り対象の0番目をNativeExcelControlクラスのコンストラクタに渡します。これでサブクラス化が完了します。
その後、サブクラス化によって新しく定義されたSelectChangedイベントにイベントハンドラを追加します。
イベントハンドラの実装は以下となります。

private void NativeExcelControl_SelectChanged(object sender, EventArgs e)
{
    this.Label2.Text = "";
    if (this.Application.Selection is Excel.Range)
    {
        this.Label2.Text = "This is a cell.";
    }
    else if (this.Application.Selection is Excel.Picture)
    {
        this.Label2.Text = "This is a picture.";
    }
}

ブックの追加

あるブックを編集している際に、別のブックを開くケースがあるかと思います。この場合、乗っ取る必要があるウインドウが2つに増えます(乗っ取り対象をリストで返しているのはこのためです)。ここでは、新しいウインドウが生成されたことを、WorkbookActivateイベントを使って検出します。
イベントハンドラの実装は以下となります。

void Application_WorkbookActivate(Excel.Workbook Wb)
{
    foreach (NativeExcelControl ctrl in ecs)
    {
        ctrl.SelectChanged -= this.NativeExcelControl_SelectChanged;
        ctrl.Dispose();
    }
    ecs.Clear();
    foreach (IntPtr ptr in NativeExcelControlHelper.FindTarget())
    {
        NativeExcelControl ctrl = new NativeExcelControl(ptr);
        ctrl.SelectChanged += new EventHandler<EventArgs>(
            this.NativeExcelControl_SelectChanged);
        ecs.Add(ctrl);
    }
}

Visual Studioでスマートデバイスプロジェクトが作成できない?

久々にWindowsMobileなプログラムでも作ろうかと思ったら、VisualStudio で新規プロジェクトを作れない。全部作れないわけじゃなくてスマートデバイスプロジェクトだけが作れない。

調べてみたら、IE8が入っていると引っかかるらしい。こないだのWindowsUpdateで入れちゃったんだな。どうせ使わないけど更新しといたせいで。

http://d.hatena.ne.jp/gust_notch/20090515/p3

自分もこれに引っかかってしばらくハマっていたので、備忘録代りに書いておきます。
元記事の方は以下で。

Visual C++ チームは、現行リリースされている Internet Explorer (Internet Explorer 8) のインストール後に、いくつかの VC++ ウィザードが正常に機能しなくなるということを発見しました。 この影響を受ける製品は、Visual Studio 2005 と Visual Studio 2008 で、以下のウィザードが影響を受けます。
l 関数の追加
l 変数の追加
l スマート デバイス – 新規プロジェクト作成
l スマート デバイス – クラスの追加
IE8 がインストールされているマシン上の Visual Studio 2005 および Visual studio 2008で、上記のいずれかのウィザードを起動すると、ポップアップ スクリプト エラーが出ます。 このエラーに遭遇された方のために、回避策があります。
以下の手順に従ってください。
l regedit を開く(64 ビット OS では、32 ビットの regedit を開いてください。)
l 以下のレジストリ キー内に(まだ存在しない場合は)1000 という名前の新しいキーを作成してください。
“HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Zones”
l 以下の内容で、作成したキーに DWORD 値を入力します。
o 名前 = 1207
o 種類 = REG_DWORD
o データ = 0x000000
なお、この回避策を Visual Studio 2005 で行なうには、Visual Studio 2005 SP1 (および Windows VistaVisual Studio 2005 SP1 アップデート)がインストールされている必要があります。

http://blogs.msdn.com/fmo_jp/archive/2009/04/02/visual-studio-2005-visual-studio-2008.aspx

半透明画像の重ね合わせ

ベースとなるフォームの上に、半透明のフォームを重ね合わせたフォームを作成します。
まず、ベースとなるフォームを作成します。

ピクチャボックスと、透明度を調節するためのスライダーを配置します。
次に、重ね合わせフォームを作成します。

ウインドウの外枠は不要なので、全て無効にします。重ね合わせフォームの大きさを、ベースとなるフォームのピクチャボックスと同じにします。
次に。重ね合わせフォーム上に、ピクチャボックスを配置します。

ピクチャボックスの大きさを重ね合わせフォームと同じにします。
次に、各々のフォームに適当な画像を貼り付けます。


最後に、イベントハンドラを実装します。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace OpacityForm
{
    public partial class BaseForm : Form
    {
        public BaseForm()
        {
            InitializeComponent();

            this.ofm.Visible = false;
            this.ofm.Opacity = (double)this.trackBar1.Value /
                (double)this.trackBar1.Maximum;
            this.ofm.Visible = true;
        }

        private void trackBar1_Scroll(object sender, EventArgs e)
        {
            this.ofm.Opacity = (double)this.trackBar1.Value /
                (double)this.trackBar1.Maximum;
        }

        private OpacityForm ofm = new OpacityForm();

        private void BaseForm_Move(object sender, EventArgs e)
        {
            this.ofm.Location = this.PointToScreen(this.pictureBox1.Location);
        }
    }
}

以上で完成です。

スライダーを動かすと、重ね合わせフォームの画像の透明度が下がっていきます。

EXCELに画像をドラッグする。

 .Netアプリケーションから、EXCELにも受け取れる形で画像をドラッグする際、通常は、DataObjectを作成し、SetImageメソッドで、Imageクラスを渡します。
 ところが、この方法だと、透過色が設定できるイメージ(GIFファイルなど)の透過色情報が抜け落ちます。
 いったんメタファイルに変換後貼り付けを行う手法もありますが、元のイメージとサイズが異なってしまうなどの不具合があります。

 そこで、HTML形式を使用して、透過情報を保持したままドラッグ開始する方法を紹介します。
 ただし、この方法を使うためには、イメージをファイルに保存しておく必要がありますので、ご注意ください。

        private void StartDrag(string fileName, Control parent)
        {
            DataObject data = new DataObject();

            string clipboardHeader = "Version:1.0\r\nStartHTML:{0:000000000}\r\nEndHTML:{1:000000000}\r\nStartFragment:{2:000000000}\r\nEndFragment:{3:000000000}\r\n";
            string htmlHeader = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">\r\n<HTML><HEAD></HEAD><BODY><!--StartFragment-->";
            string imgBody = "<IMG src=\"file:///{0}\">";
            string htmlFooter = "<!--EndFragment--></BODY></HTML>";

            string imgFull = string.Format(imgBody, fileName.Replace(@"\", "/"));

            int startHtml = System.Text.Encoding.UTF8.GetByteCount(string.Format(clipboardHeader, 0, 0, 0 ,0));
            int endHtml = System.Text.Encoding.UTF8.GetByteCount(htmlHeader + imgFull + htmlFooter) + startHtml - 1;
            int startFragment = System.Text.Encoding.UTF8.GetByteCount(htmlHeader) + startHtml;
            int endFragment = System.Text.Encoding.UTF8.GetByteCount(imgFull) + startFragment - 1;

            string clipBoardHeaderFull = string.Format(clipboardHeader, startHtml, endHtml, startFragment, endFragment);

            string html = clipBoardHeaderFull + htmlHeader + imgFull + htmlFooter;

            byte[] bytes = System.Text.Encoding.UTF8.GetBytes(html);
            data.SetData(DataFormats.Html, new MemoryStream(bytes));

            parent.DoDragDrop(data, DragDropEffects.Copy);
        }

HTML形式のクリップボードフォーマットを作成し、DataObjectに設定します。

コツとしては、クリップボード内データがUTF-8なので、UTF-8に変換してやり取りする必要がある点です。
そのため、Encodingクラスを使用して、UTF-8に変換したものをSetDataで渡しています。

data.SetDataの部分をClipboard.SetDataに置き換えれば、クリップボードへの貼り付けも同様に可能だと思います。

#なお、例外処理や、後処理が抜けていますのでご注意ください。

以下のページを参考にいたしました。
HTML Clipboard Format (Internet Explorer) | Microsoft Docs

任意の形のウィンドウを作る

デスクトップアクセサリや、ガジェットを作成する場合は、ウインドウを任意の形に変更したいと考えることが多いと思います。ここでは、2通りの方法を説明します。

System.Windows.Forms.Form.TransparencyKeyプロパティを使う

おそらく最も簡単な方法だと思います。
背景画像を指定し、透過させる色を指定すると、透過色の部分が描画されなくなります。
この画像の黒い部分を透明化します。
実コードは以下となります。

    public partial class FormTransparensyKey : Form
    {
        public FormTransparensyKey()
        {
            InitializeComponent();

            this.DoubleBuffered = true;
        }

        private void FormTransparensyKey_Load(object sender, EventArgs e)
        {
            this.FormBorderStyle = FormBorderStyle.None;
            Bitmap bmp = new Bitmap(@"RegionVSTransparensyKey.bmp");
            Color transColor = bmp.GetPixel(0, 0);
            this.Size = bmp.Size;
            bmp.MakeTransparent(transColor);
            this.BackgroundImage = bmp;
            this.BackColor = transColor;
            this.TransparencyKey = transColor;
        }


実際に動かしてみると、背景色部分が大きいせいもありますが、若干のちらつきがあります。
また、描画速度は、概ね80ms程度でした。

System.Windows.Forms.Control.Regionプロパティを使う

描画領域をあらかじめ指定しておくと、指定領域の中のみが描画されます。

実行結果は左のようになります(背景はデスクトップの壁紙です)。
ここでは、描画する領域を、100×100の矩形に収まる円にしています。


実コードは以下となります。

    public partial class FormControlRegion : Form
    {
        public FormControlRegion()
        {
            InitializeComponent();

            this.DoubleBuffered = true;
        }

        private void FormControlRegion_Load(object sender, EventArgs e)
        {
            System.Drawing.Drawing2D.GraphicsPath path =
                new System.Drawing.Drawing2D.GraphicsPath();
            path.AddEllipse(new Rectangle(0, 0, 100, 100));
            this.Region = new Region(path);
        }

実際に動かしてみると、背景部分の再描画が無いせいか、特にちらつきはありませんでした。
描画速度は、概ね70ms程度でした。

画像からリージョンを生成する(System.Windows.Forms.Control.Regionプロパティを使う)

System.Windows.Forms.Form.TransparencyKeyプロパティを使う場合、描画領域が自動的に認識されるため、任意の画像で透過処理が可能ですが、背景色部分の描き直しが発生するせいか、若干のちらつきが出てしまいます。
System.Windows.Forms.Control.Regionプロパティを使う場合、ちらつきは発生しませんが、描画領域を、リージョンのデータとして渡してやる必要があります。これを手で作るのは困難です。
そこで、元画像を解析し、リージョンのデータを自動的に生成するコードを追加しました。
実コードは以下となります。

        private void FormControlRegion_Load(object sender, EventArgs e)
        {
            this.Region = GetRegionFromBitmap(@"RegionVSTransparensyKey.bmp");
        }

        private Region GetRegionFromBitmap(string filename)
        {
            bitmap = new Bitmap(filename);

            Color transColor = this.bitmap.GetPixel(0, 0);
            this.Size = this.bitmap.Size;
            this.BackgroundImage = this.bitmap;

            System.Drawing.Drawing2D.GraphicsPath gpath
                = new System.Drawing.Drawing2D.GraphicsPath();

            unsafe
            {
                BitmapData dataRgb = this.bitmap.LockBits(
                    new Rectangle(0, 0, bitmap.Width, bitmap.Height),
                    ImageLockMode.ReadOnly,
                    PixelFormat.Format24bppRgb);

                int width = this.bitmap.Width;
                int height = this.bitmap.Height;
                int stride = dataRgb.Stride;
                int maxX = stride / 3;
                int modX = stride % 3;
                byte* currentData = (byte*)dataRgb.Scan0 - 1;
                for (int y = 0; y < height; ++y)
                {
                    for (int x = 0; x < maxX; x++)
                    {
                        Color pixcel = Color.FromArgb((int)*++currentData,
                            (int)*++currentData, (int)*++currentData);
                        if (pixcel != transColor)
                        {
                            gpath.AddRectangle(new Rectangle(x, y, 1, 1));
                        }
                    }
                    currentData += modX;
                }
                this.bitmap.UnlockBits(dataRgb);
            }

            return new Region(gpath);
        }

Regionクラス(System.Drawing)は、GraphicsPathクラス(System.Drawing.Drawing2D)で定義された、接続された一連の図形からなる領域です。GraphicsPathクラスは、任意の数の図形の集合で、複雑な図形を表現します。ここでは、描画するピクセルを1つずつ、GraphicsPathクラスに追加していくことで、描画領域を定義しています。

実行結果は左のようになります(背景はデスクトップの壁紙です)。
描画に要する時間は、概ね99ms程度でした。

ちらつきの解消(System.Windows.Forms.Form.TransparencyKeyプロパティを使う)

Form.FormBoderStyleにNoneを指定すると、ベースとなるフォームの描画を行わないので、ちらつきが無くなります。
描画速度は、ちらつき対策を行う前と同様に、概ね80ms程度でした。

まとめ

今回の試行では、System.Windows.Forms.Form.TransparencyKeyプロパティを使う方が若干速い、という結果になりましたが、複数の画像を切り替えて表示させる場合などでは、一度生成したリージョンを使いまわすことが出来るSystem.Windows.Forms.Control.Regionプロパティを使用した方が速くなるケースもあるかと思います。