2015年03月03日

C#で透過画像のコントロール

長期放置していきなりですが、今現在パートでプログラマーに復帰しています んで、あんまり見た目をいじることは今までやっていなかったのでちょっと苦労した内容をぼそぼそっと書いてみます やりたいことその1  フォームを透過pngの形にしたい   フォームのコンストラクタで背景画像に透過pngを指定してから以下の設定で実現しました
            this.TransparencyKey = this.BackColor;
            this.Opacity = 1;
大雑把にいうと ・透過色に背景色を指定 ・透過率を100%(1)に指定 やりたいことその2  ボタンも透過pngで作りたい   フォームと同じようにしようとしてもプロパティが無い   頑張って透過できるように派生クラスを作ったけど、ボタンのイベントを受け取る領域に変化が無いため、   透過武運でもクリックイベントが発生してしまう   =>ボタンの形状を変えてやればいいんじゃね?   というわけで四苦八苦して透過画像から輪郭を取得して輪郭情報を元にボタンの形状を変化する処理を作りました まずは画像(Bitmapクラスのインスタンス)から透過部分の輪郭となる座標のリストを取得 なおソースコードがunicodeなおかげで漢字を使ってもコンパイルエラーは起きません 英語が苦手でも安心ですね(ということにしてください)
        private List get境界(Bitmap bmp)
        {
            List retPList = new List();

            // 左からの境界点を取得
            for (int yPoint = 0; yPoint < bmp.Height; yPoint++)
            {
                Boolean isLine = false;
                if (bmp.GetPixel(0, yPoint).A > 0)
                {
                    isLine = true;
                }
                for (int xPoint = 0; xPoint < bmp.Width; xPoint++)
                {
                    Boolean isView = false;
                    int iOffset = -1;
                    if (bmp.GetPixel(xPoint, yPoint).A > 0)
                    {
                        isView = true;
                        iOffset = 0;
                    }
                    if (isLine != isView)
                    {
                        retPList.Add(new Point(xPoint + iOffset, yPoint));
                        isLine = isView;
                    }
                }
            }

            // 上からの境界点を取得
            for (int xPoint = 0; xPoint < bmp.Width; xPoint++)
            {
                Boolean isLine = false;
                if (bmp.GetPixel(xPoint, 0).A > 0)
                {
                    isLine = true;
                }
                for (int yPoint = 0; yPoint < bmp.Height; yPoint++)
                {
                    Boolean isView = false;
                    int iOffset = -1;
                    if (bmp.GetPixel(xPoint, yPoint).A > 0)
                    {
                        isView = true;
                        iOffset = 0;
                    }
                    if (isLine != isView)
                    {
                        retPList.Add(new Point(xPoint, yPoint + iOffset));
                        isLine = isView;
                    }
                }
            }

            return retPList.Distinct().ToList();
        }
最初は上から順に1行ごと、左から透過/非透過の切り替わりをチェックして切り替わったところを境界線としていましたが、 透過ピクセルの座標を輪郭とするとおかしなことになりそうだったので透過ー>非透過に切り替わった際は現在座標ですが、 非透過ー>透過の切り替えの場合は一つ前の座標をリストに追加しいています また、これだけだと 透透透透非非非 透透非非非非非 となった際に座標が飛んでしまうため、左からの列ごとに上からの切り替わり座標の取得も行い、最終的に重複座標を削除して輪郭に使える座標のみのリストを取得しています 次に座標リストから隣の座標を検索する処理を組み込んでいます get境界ではすべての切り替わり座標を取得しているので左右の端だけでなく内部にも複数の座標がある場合でも切り替わりの座標を取得しているため、 全てを輪郭として使おうとすると大変な形になってしまいます
        private List get隣接境界(List pList)
        {
            List retPList = new List();
            List srcPList = new List(pList);
            Point pBase = pList[0];
            Point pSarch;

            retPList.Add(pBase);
            srcPList.Remove(pBase);
            Double dbl角度 = 0.0;
            while (srcPList.Count(m => is隣接(pBase, m)) > 0)
            {
                pSarch = srcPList.Select(m => { is隣接(pBase, m); return m; }).OrderBy(m => get距離(dbl角度, pBase, m)).First();
                retPList.Add(pSarch);
                srcPList.Remove(pSarch);
                dbl角度 = Math.Atan2(pBase.Y - pSarch.Y, pBase.X - pSarch.X);
                pBase = pSarch;
            }

            return retPList;
        }
というわけでこちらは境界座標のリストから一つ目の座標を基点として繋がっている座標を次々と取得して隣接した座標を順番に取得するようにしています また一つ前の基点と現在の基点の角度を元に一番近い次の境界の座標を取得するようにしています まずは隣接している座標がかどうかのチェックの為に関数を一つ用意しています pBaseが基点でpTargetが隣接しているかどうか確認する座標です
        private Boolean is隣接(Point pBase, Point pTarget)
        {
            Boolean bRet = false;

            if (((Math.Abs(pBase.X - pTarget.X) == 0) || (Math.Abs(pBase.X - pTarget.X) == 1))
                && ((Math.Abs(pBase.Y - pTarget.Y) == 0) || (Math.Abs(pBase.Y - pTarget.Y) == 1)))
            {
                bRet = true;
            }

            return bRet;
        }
こちらは単純にX・Y座標の差が1以下であることの確認だけです get隣接境界の中でis隣接により基点に隣接している座標だけのリストを取得していますが優先順位として距離(斜めよりも上下左右を優先)の次に角度です 進行方向に対しての幅でなく、前の基点から現在の基点までの直線に対してどれだけ角度の差があるかどうかを見ています 万が一重なった場合にも同じ輪郭をなぞるようになっているはずです
        private Double get距離(Double 角度base, Point pBase, Point pTarget)
        {
            Double dbl角度 = Math.Atan2(pBase.Y - pTarget.Y, pBase.X - pTarget.X);
            Double 角度差 = (dbl角度 + 角度base) * 180 / Math.PI;

            角度差 = ((角度差 > 360) ? 角度差 - 360 : (角度差 <= 0 ? 角度差 + 360 : 角度差)) / 3600;

            return Math.Abs(pBase.X - pTarget.X) + Math.Abs(pBase.Y - pTarget.Y) + 角度差;
        }
角度の差をそのまま確認する場合はMath.Atan2で求めた数値で行けますが、 計算で1周したの境目を超えてしまうとおかしなことになるので計算で1週分の増減が必要になることがあります その際に〜度の数値にしないとわかりにくいので係数をかけて調整しています なお、最終的に360で割ってしまうだけだと1未満の数値で距離の差を超えてしまうかと思って更に10で割った数値を出すために3600で割っています が、結局斜めを√2で処理するよりもマスの移動数でやったほうが楽だったので360で割っても大丈夫になっていますが面倒なので変更していません そして距離に確度の開きを追加することで優先順位を出力し、get隣接境界関数内で並び替えた後に最初の物を取得することで一番優先順位の高い座標を取得しています なお、前の角度と検査対象の角度比較を足す場合と引く場合で優先順位を時計回りなのか反時計回りなのかが決まります 眠いのでどっちなのか確認していません、大体の理屈だけで作りました コントロールの整形処理です、関数名に3が付くのは3番目に試して完成したからで深い意味はありません
        private void setContololRegion3(Control target, Bitmap bmp)
        {

            System.Drawing.Drawing2D.GraphicsPath path = new System.Drawing.Drawing2D.GraphicsPath();
            List pList = get境界(bmp);

            //get境界
            while(pList.Count>0)
            {
                List pChainList = get隣接境界(pList);
                if(pChainList.Count==0)
                {
                    path.AddPath(new System.Drawing.Drawing2D.GraphicsPath(pList.ToArray(), Enumerable.Repeat((byte)System.Drawing.Drawing2D.PathPointType.Line, pList.Count).ToArray()), false);
                    pList.Clear();
                }
                else
                {
                    path.AddPath(new System.Drawing.Drawing2D.GraphicsPath(pChainList.ToArray(), Enumerable.Repeat((byte)System.Drawing.Drawing2D.PathPointType.Line, pChainList.Count).ToArray()), false);
                    pChainList.ForEach(m => pList.Remove(m));
                }

            }

            target.Width = bmp.Width + 10;
            target.Height = bmp.Height + 10;
            target.Region = new Region(path);
        }
後はもう上記の関数を活用して取得した境界線のリストをSystem.Drawing.Drawing2D.GraphicsPathにそれぞれ独立した線として登録して登録したら境界座標のリストから削除して減らしていっています 最終的に座標リストがなくなったらRegionクラスのインスタンスを生成してコントロールにぶち込めば完了です コントロールによっては枠線があるので上下左右でブサイクになりましたのでサイズを画像より高さ・横幅ともに+10して余裕を作っています これを忘れると折角取得した形をコントロールの高さ・幅でぶった切った形になってしまいますので要注意です 割と汎用的にできたと思いますので、必要になった方はお試しください
posted by 煉 at 07:39| 大阪 ☀| Comment(0) | TrackBack(0) | プログラム | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
コメントを書く
お名前: [必須入力]

メールアドレス:

ホームページアドレス:

コメント: [必須入力]

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。

この記事へのトラックバック
×

この広告は1年以上新しい記事の投稿がないブログに表示されております。