2015年7月24日金曜日

PictureBoxで右クリック等を読み取る

C#のPictureBoxでクリック状態を検出する場合、Clickイベントでは右クリックを検出することは出来ません。右クリックを読み込むにはMouseDownイベントを使用する必要があります。

using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace PictureBoxClick {
    public partial class Form1 : Form {
        public Form1() {
            InitializeComponent();

            pictureBox1.MouseDown += pictureBox1_MouseDown;
            pictureBox1_MouseDown(pictureBox1, new MouseEventArgs(MouseButtons.None, 0, 0, 0, 0));
        }

        private void pictureBox1_MouseDown(object sender, MouseEventArgs e) {
            bool LeftClick = MouseButtons.None != (e.Button & MouseButtons.Left);
            bool RightClick = MouseButtons.None != (e.Button & MouseButtons.Right);
            bool MiddleClick = MouseButtons.None != (e.Button & MouseButtons.Middle);
            bool XButton1Click = MouseButtons.None != (e.Button & MouseButtons.XButton1);
            bool XButton2Click = MouseButtons.None != (e.Button & MouseButtons.XButton2);
            Point Location = e.Location;

            Bitmap bmp = new Bitmap(pictureBox1.Width, pictureBox1.Height);

            using (Graphics gra = Graphics.FromImage(bmp))
            using (Font font = new Font("MS ゴシック", 12)) {
                gra.Clear(Color.Black);

                StringBuilder sb = new StringBuilder();

                sb.Append("Left    :" + LeftClick + "\n");
                sb.Append("Right   :" + RightClick + "\n");
                sb.Append("Middle  :" + MiddleClick + "\n");
                sb.Append("XButton1:" + XButton1Click + "\n");
                sb.Append("XButton2:" + XButton2Click + "\n");
                sb.Append("Location:" + Location + "\n");

                gra.DrawString(sb.ToString(), font, Brushes.White, new PointF());
            }

            if (pictureBox1.Image != null) {
                pictureBox1.Image.Dispose();
            }
            pictureBox1.Image = bmp;
        }
    }
}

Leftは左、Rightは右、Middleはホイール、XButton1は戻るボタン、XButton2は進むボタン が割り当てられています。またこれらはMouseButtons列挙体のORされた値ですし、実際にnew MouseEventArgsで各ボタンをORして渡せば複数のボタンクリック状態を引数に与えることができます。しかし、実際の動作においては複数ボタンをクリックした場合、クリックした順番で複数回のイベントが発生します。これはクリックしたタイミングの問題というより、内部処理の問題のようです。例えば、user32.dllを使用して左クリックと右クリックをORして同時に行わせた場合、2回のイベントが発生します。基本的にMouseDownイベントだけではマウスの同時押しによるファンクションは行えないと考えたほうがいいでしょう。

どうしても同時押しを検出したい場合は以下のようにします。

using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace PictureBoxClick {
    public partial class Form1 : Form {
        bool LeftClick, RightClick, MiddleClick, XButton1Click, XButton2Click;

        public Form1() {
            InitializeComponent();

            pictureBox1.MouseDown += pictureBox1_MouseDown;
            pictureBox1.MouseUp += pictureBox1_MouseUp;
            pictureBox1_MouseDown(pictureBox1, new MouseEventArgs(MouseButtons.None, 0, 0, 0, 0));
        }

        private void pictureBox1_MouseDown(object sender, MouseEventArgs e) {
            System.Diagnostics.Debug.WriteLine(e.Button.ToString());

            if (MouseButtons.None != (e.Button & MouseButtons.Left)) {
                LeftClick = true;
            }

            if (MouseButtons.None != (e.Button & MouseButtons.Right)) {
                RightClick = true;
            }

            if (MouseButtons.None != (e.Button & MouseButtons.Middle)) {
                MiddleClick = true;
            }

            if (MouseButtons.None != (e.Button & MouseButtons.XButton1)) {
                XButton1Click = true;
            }

            if (MouseButtons.None != (e.Button & MouseButtons.XButton2)) {
                XButton2Click = true;
            }

            Draw();
        }

        private void pictureBox1_MouseUp(object sender, MouseEventArgs e) {
            System.Diagnostics.Debug.WriteLine(e.Button.ToString());

            if (MouseButtons.None != (e.Button & MouseButtons.Left)) {
                LeftClick = false;
            }

            if (MouseButtons.None != (e.Button & MouseButtons.Right)) {
                RightClick = false;
            }

            if (MouseButtons.None != (e.Button & MouseButtons.Middle)) {
                MiddleClick = false;
            }

            if (MouseButtons.None != (e.Button & MouseButtons.XButton1)) {
                XButton1Click = false;
            }

            if (MouseButtons.None != (e.Button & MouseButtons.XButton2)) {
                XButton2Click = false;
            }

            Draw();
        }

        void Draw() {

            Bitmap bmp = new Bitmap(pictureBox1.Width, pictureBox1.Height);

            using (Graphics gra = Graphics.FromImage(bmp))
            using (Font font = new Font("MS ゴシック", 12)) {
                gra.Clear(Color.Black);

                StringBuilder sb = new StringBuilder();

                sb.Append("Left    :" + LeftClick + "\n");
                sb.Append("Right   :" + RightClick + "\n");
                sb.Append("Middle  :" + MiddleClick + "\n");
                sb.Append("XButton1:" + XButton1Click + "\n");
                sb.Append("XButton2:" + XButton2Click + "\n");

                gra.DrawString(sb.ToString(), font, Brushes.White, new PointF());
            }

            if (pictureBox1.Image != null) {
                pictureBox1.Image.Dispose();
            }
            pictureBox1.Image = bmp;
        }
    }
}

このように、DownでフラグをTrueにし、UpでFalseにすれば、そのフラグを確認することにより任意のタイミングでマウス状態を獲得できます。ただ、この方法でも検出出来ない場合があることに注意が必要です。僕の環境では左・中・右の同時押しは検出できましたが、戻る・進む・右の同時押しは検出できませんでした。
そもそもマウスボタンの同時押しなど通常の動作状態ではありませんから、余程の理由がない限りは使用するべきではないと思います。

2015年7月22日水曜日

モデルロケットを撮りたい場合

今まで何回かモデルロケットを撮っているので、撮影条件をまとめてみようと思います。


シャッター遅めでロケットがブレるような写りの場合。高速感が有ってよろしい。しかしデザインはわからない。

ロケットが十分に加速していない場合(点火直後の場合)は640分の1秒でもデザインがわかる。

読みづらくて申し訳ないです。ランチロッドを抜けてしばらく経ったあたり。かなりの速度の状態だと1000分の1秒でもブレ気味。

開傘直後あたりまでは結構速力がある。ただしこの辺りになるとシャッタースピードよりも、フォーカスが大変。なんせ発射台と最上点では距離が2倍とか3倍とかになるので。


どうしても、絶対にモデルロケットを撮りたい場合、設定はシャッター優先モードで2000分の1秒以上か、マニュアルで設定するといいと思う。上空は地上よりも明るいので、マニュアルの場合は上空を撮った時に白飛びしない程度に設定。明るさは後からPsとかLrとかで修正する。フォーカスはマニュアルでランチャーに焦点を設定。上空で余裕があればマニュアルなりオートなりで再合焦させる(大抵の場合は上手く合焦しない)。とにかく数を撮って偶然を当てにする。撮影モードは連写。高級な(バッファーが多い)カメラほど有利になる。

イベントを記録したいなら、例えば子どもと参加している親御さんなら、カメラはロケット側ではなく、子供を撮影するべき。あの笑顔は大抵の場合ロケット打ち上げの写真を捨てるに値する。ロケットが写ってる写真は、発射前と発射後に手に持ってる写真を撮ればいい。大抵は打ち上げ後のほうがいい写真だと思うけど、万が一木に引っかかったとかで回収できなかった時のために、打ち上げ前にもとっておく。打ち上げ直前だと不安かもしれないので、モデルロケットを作り終わった時にも撮っておくといいかも。ノリがいい子供ならカメラを構えるとモデルロケットを付き出して自慢してくれる。


おまけ


ハイブリッドロケットを撮る場合。とにかく高速なシャッター(数千分の1秒)で、とにかく拡大して撮る。見失う?気にするな。ショックダイヤモンドはカッコいいぞ。


「ロケットを撮りたいです!カメラを買いたいんですけど何がいいですか?」って人はあんまりいないと思うけど、解像度が高いカメラは後からトリミングできるので広角気味のレンズで撮れる。広角レンズだと見失うことが少ないのでよろしい。連写は機能が有ればあんまり違いはないんじゃないかな。むしろバッファが多いほうがいい。20枚連写程度じゃ物足りなくなる。RAW記録できると後から現像するときにWBとか明るさとかいじりやすいけど、数百枚も撮ると見るのも嫌になる。 
個人的にはPENTAXをおすすめしておく(完全に個人の偏見で申し訳ないが)。ペンタのカメラは大抵が防水モデルで、汚れても丸ごと水洗いできるので、ハイブリッドロケットとかを打てるような辺鄙な場所で、転んでカメラを汚したとしても安心して対応できる。

2015年7月20日月曜日

60rdマガジンを買った

SUREFIREの60rdマガジンサイズの電動ガン用マガジンを買いました。

左が通常のマガジンサイズで、右2本が60rdマガジンサイズです。

中央が改造済み、右が未改造です。通常は850発のゼンマイマガジンです。850発だと面白く無いのでノーマルマガジンのメカを突っ込んでいます。ノーマルマグは80発程度ですから、おおよそリアルカウントっぽくなります。またゼンマイマガジンでは動かすとシャカシャカうるさいですが、ノーマルマグであれば静かです。

M4に装着するとこんな感じです。上の写真を見ても分かる通り、60rdマガジンはあまり長さは変わらないので、横からみると普通のM4に見えます。前から見るとフレームくらいの幅があるのでちょっとイカツイかもですが。

ディスカバリーチャンネルでの謎の資料映像でドアエントリーを行う隊員が長いマガジンを使用していました。通常マガジンの2倍か3倍の装弾数になるので、中でどれくらい撃つか分からない場合には有効だと思います。
反面、長さがある分伏せて撃つのは大変です。そのため、遠距離で掩体に寝そべりながら撃つような用途では使いづらいでしょう。



最後に公式動画を1つ。動画の中では100rdのマガジンを使用しています。さすがに100rdになるとかなり長く、確かにキモいなぁ という感じです。

2015年7月16日木曜日

ストロボビーコン(レプ)を買ってみた

"FMA S&S Precisionタイプ マンタ ストロボライト"というレプリカストロボを買ってみました。購入店はモケイパドック通販です。ちなみに通販では4298円ですが、箱には店頭販売用の3980円というシールがついていました。それとおそらく色の確認用ですが、小さな緑のシールも貼ってありました。裏にIR & GREENって書いてあるのにね。


外観はこんな感じ。左上の部分からCR123を入れる。そしてキャップについてるオルタネイトスイッチで全体のON/OFFを行う。左右に付いているスイッチを両方押して長押しすると、離した時から可視光が光る。一旦可視光になったらオルタネイトスイッチでOFFにしてからONにしないとIRに戻らないみたい。
システムONになった時にバイブレータモータで3回振動する。が、振動は大きくないので手に持ってないとわからないと思う。それと、振動よりも駆動音のほうが大きいので、そっちのほうが判断に使いやすい。また、可視光の時はバイブは動かないみたい。

緑では大きな2個のLEDが光る。かなり眩しい。IRの時は3個のLEDが点灯する。IRはもちろん肉眼では見えないが、全く見えないわけではなく、若干可視側に漏れがあるので、日没後のある程度暗くなった時点で目で見えるようになる。

点灯時はこんな感じ。めちゃくちゃ明るい。IRもNVで見れば相当に明るい。パルス点灯だからいいものの、ヘタすると増幅管が焼けるようなレベル。3mほど離れたところから見ても増幅管の発光が眩しい。やはりビーコンは遠くから見るものだ。

内蔵されてるLEDは秋月で10個150円とか200円とかで売ってるような見た目。なんともチープ。

このストロボ、ヘルメットに装着することを前提に作られているので、ベルクロ面は曲面になっている。なので平面、例えばプレートキャリアとかバックパックのベルクロには綺麗に貼れない。くっつかないこともないが、ちょっと不安な感じ。

また落下防止のワイヤを通す穴がないので、確実に固定しておかないと、気がついたら紛失していた ということになりかねない。正確には穴はあるのだけど、細い針金1本通る程度の穴なので、柔軟性があり切れ辛い糸を通しておく必要がある。


一応NVで動画もとってみた。が、あんまり綺麗に見えないね。肉眼だと綺麗に見えるんだけど、光学系がどうしようもない。




これは買いか? と聞かれると悩む。正直、コスプレ以外には使い道がない。というのは、例えばLITEBUCK LEDマーカーであれば点滅以外にも点灯ができるので、普通に明かりとして使えるし、赤色なら目にも優しいので天体観測時に非常に役に立つ(なにせ-20℃でも水没しても使えるのだ。これほど信頼できるアイテムは数多くないだろう)。しかしこのビーコンではIR/可視光の点滅しかできない。そのためにサバゲでNVを使用した相手に対するビーコンとしてのみしか使えないのだ。
というわけで、積極的にオススメできるものではない。

C#のContextMenuStripでどのコントロールをクリックしたのかを知る

C#で右クリックするにはContextMenuStripを使用します。基本的にはToolStripMenuItemのクリックイベントを使用します。しかしそれだけでは「ContextMenuStripに設定されたどれか」しかわかりません。ということでどうにかします。が、結構簡単なのでソースコードを見たほうが楽でしょう。

using System;
using System.ComponentModel;
using System.Windows.Forms;

namespace ContextMenuStripTest {
    public partial class Form1 : Form {
        Control ContextMenuStrip1_Source;

        public Form1() {
            InitializeComponent();

            contextMenuStrip1.Opening += contextMenuStrip1_Opening;
            copyToolStripMenuItem.Click += CopyToolStripMenuItem_Click;
        }

        private void contextMenuStrip1_Opening(object sender, CancelEventArgs e) {
            ContextMenuStrip Menu = sender as ContextMenuStrip;

            if (Menu == null) {
                return;
            }

            ContextMenuStrip1_Source = Menu.SourceControl;
        }

        private void CopyToolStripMenuItem_Click(object sender, EventArgs e) {
            if (ContextMenuStrip1_Source == null) {
                return;
            }

            Clipboard.SetText(ContextMenuStrip1_Source.Text);
        }
    }
}

OpeningイベントでContextMenuStrip.SourceControlを獲得し、広域変数に設定します。このSourceはControl型です。そのためControlから継承された、例えばTextなどはそのまま使用することができるため、いくつかのテキストボックスに1つのContextMenuStripを設定し、そのテキストボックスに入っている文字列を獲得する ということが出来ます。

2015年7月11日土曜日

線分と線分の当たり判定

線分同士の当たり判定が欲しくなったので試してみた
C#のFormで、FormにはPictureBoxを親コンテナにドッキングして配置する。またPictureBoxのClickイベントを使用する。
青い斜めの線は固定となり、もう一方の線はマウスで二箇所をクリックすることにより、それらを接続する線を引くことができる。2本の線が交差している場合は赤で、交差していない場合は緑で表示する。
アルゴリズムは 線分交差判定 - juntkの日記 に書いてある物をそのまま使用させてもらった。

線分同士の当たり判定は、例えば現在位置と未来位置で線を作り、進入禁止エリアを囲うラインと当たり判定を行い、衝突が発生した場合は移動させない というような使い方ができると思う。ただし線分同士の衝突範囲では、万が一線で囲ったエリアの中に入り込んでしまった場合には、外にでることができなくなるという問題がある。また、今いる場所が進入禁止エリアの中なのか、それとも稼働可能範囲なのかを判定することは出来ない。

それと、線分同士の当たり判定では始点と終点を使用するため、線のどの部分で衝突しているかを判定することは出来ない。もしもどの場所で衝突しているかを調べる必要があるなら、二分探索等で十分に線が短くなるまで探していくか、他の方法を考える必要がある。


using System;
using System.Drawing;
using System.Windows.Forms;

namespace LineCollisionDetection {
    public partial class Form1 : Form {
        LinePoint Line1, Line2;
        Point? Start;
        Pen Pen1, Pen2, Pen3;

        public Form1() {
            InitializeComponent();

            Pen1 = new Pen(Color.Red, 3);
            Pen2 = new Pen(Color.Green, 3);
            Pen3 = new Pen(Color.Blue, 3);

            Line1 = new LinePoint(new Point(100, 100), new Point(300, 300));
            Line2 = new LinePoint(new Point(300, 100), new Point(100, 300));

            Size += new Size(400, 400) - pictureBox1.Size;
            MaximumSize = MinimumSize = Size;

            Draw();
        }

        ~Form1() {
            Pen1.Dispose();
            Pen2.Dispose();
            Pen3.Dispose();

            if (pictureBox1.Image != null) {
                pictureBox1.Image.Dispose();
            }
        }

        private void pictureBox1_Click(object sender, EventArgs e) {
            Point mouse = pictureBox1.PointToClient(Cursor.Position);

            if (Start == null) {
                Start = mouse;
            } else {
                Line1 = new LinePoint(Start ?? new Point(), mouse);
                Start = null;

                Draw();
            }
        }

        public void Draw() {
            Bitmap bmp = new Bitmap(pictureBox1.Width, pictureBox1.Height);

            using (Graphics gra = Graphics.FromImage(bmp)) {
                bool Collision = LinePoint.IsCollision(Line1, Line2);

                gra.DrawLine(Pen3, Line2.S, Line2.E);
                gra.DrawLine(Collision ? Pen1 : Pen2, Line1.S, Line1.E);
            }

            if (pictureBox1.Image != null) {
                pictureBox1.Image.Dispose();
            }
            pictureBox1.Image = bmp;
        }

        class LinePoint {
            public Point S;
            public Point E;

            public LinePoint() {
                S = new Point();
                E = new Point();
            }

            public LinePoint(Point S, Point E) {
                this.S = S;
                this.E = E;
            }

            public LinePoint(LinePoint Line) {
                S = Line.S;
                E = Line.E;
            }

            static public bool IsCollision(LinePoint Line1, LinePoint Line2) {
                return ((
                    ((Line2.S.X - Line2.E.X) * (Line1.S.Y - Line2.S.Y) + (Line2.S.Y - Line2.E.Y) * (Line2.S.X - Line1.S.X)) *
                    ((Line2.S.X - Line2.E.X) * (Line1.E.Y - Line2.S.Y) + (Line2.S.Y - Line2.E.Y) * (Line2.S.X - Line1.E.X)) <= 0) && (
                    ((Line1.S.X - Line1.E.X) * (Line2.S.Y - Line1.S.Y) + (Line1.S.Y - Line1.E.Y) * (Line1.S.X - Line2.S.X)) *
                    ((Line1.S.X - Line1.E.X) * (Line2.E.Y - Line1.S.Y) + (Line1.S.Y - Line1.E.Y) * (Line1.S.X - Line2.E.X)) <= 0));
            }
        }
    }
}

2015年7月10日金曜日

ゲームパッドの値をシリアルポートに吐き出す

ゲームパッドを使いたい と思うことは度々あると思います。が、面倒ですよね? Javaとかいう言語を使ったりするのは。ということでC#でやってみたいと思います。
ここでは条件として、Xbox360のコントローラに限定したいと思うます。そうすることでC#で簡単に使えるようになります。

Xbox360のコントローラなんてダサいって? このコントローラは米軍も使用している由緒正しいコントローラです!

さて、今回はXNA Game Studioというフレームワークを使用しました。これは既に開発が終了していますが、C#でゲームを動かすために作られたもので、2Dや3Dなどのレンダリングをすることが出来ます(もっとも、レンダリングをすること"しか"できないので、3Dの表現は大変ですが)。
また、XNAはXboxのゲーム開発環境としても想定されており、年間所定のライセンスを支払うとXboxで遊んだり、安価に販売することも出来たようです。そのために、XNAにはXbox360のコントローラを使用するためのGamePadStateというクラスが用意されています。

この機能を使用するためにはまず参照設定でMicrosoft.Xna.Frameworkを読み込む必要があります。また表示のためにPictureBoxをフォームに貼り付け、AnchorをTop, Bottom, Left, Rightに設定して下さい。そしてFormにはLoadとFormClosedのイベントを作成し、またTimerとSerialPortのコンポーネントを追加して下さい。
今回のGUIではシリアルポートのOpen・Close処理は起動時・終了時に行います。そのためSerialPortコンポーネントで使用するCOMポート名と通信フォーマットを適切に設定しておいて下さい。
それから、Timerはゲームパッドの読み込みに使用します。Enableはtrueにし、Intervalは適当な値(50-500程度)に設定して下さい。

実際にデータを読んで送信するための処理はソースコードを読んでみてください。画像にして表示する部分が半分以上で、実際の送受信処理は非常に簡単です。

今回は左スティックのみを使用しましたが、XNAではXboxボタン(中央にある椎茸みたいなボタン)以外のすべてのボタンを読み込むことが出来ます。そのため、XBee等を使用して戦車のラジコンを操縦しようと思えば、FPSゲームと全く同じコントロール方法で制御することも可能になります(もちろん操作対象にマイコンを組み込んで操作を理解させる必要がありますが)。また、情報を読み込む以外にも、コントローラの振動を制御することも可能です。これは例えば戦車のラジコンであれば、ラジコン側に加速度センサを搭載しておき、一定以上の振動が検出されたらゲームパッドを振動させる事により、段差を乗り越えたらコントローラを通じて操縦者に伝えることが出来ます。他にも、主砲を撃つ操作をした時に振動させるなどの演出もできるでしょう。
また、XNAではGamePad.GetStateの引数にPlayerIndex.Oneを渡していますが、これは1-4までを指定できます。つまり1台のPCで最大4台のコントローラを区別することが出来ます。これは4台の戦車で対戦したりとか、1個の複雑なロボットアームを複数人で操作することが可能になります。


using System;
using System.Drawing;
using System.Windows.Forms;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;

namespace XnaPortDump {
    public partial class Form1 : Form {
        public Form1() {
            InitializeComponent();

            Size += new Size(400, 400) - pictureBox1.Size;
        }

        private void timer1_Tick(object sender, EventArgs e) {
            GamePadState gamePadState = GamePad.GetState(PlayerIndex.One);

            float x = gamePadState.ThumbSticks.Left.X;
            float y = gamePadState.ThumbSticks.Left.Y;

            Bitmap bmp = new Bitmap(pictureBox1.Width, pictureBox1.Height);
            using (Graphics gra = Graphics.FromImage(bmp)) {
                gra.FillEllipse(Brushes.Lime, bmp.Width / 2 + (x * bmp.Width / 2 * 0.95f) - 5, bmp.Height / 2 - (y * bmp.Height / 2 * 0.95f) - 5, 10, 10);
            }

            if (pictureBox1.Image != null) {
                pictureBox1.Image.Dispose();
            }
            pictureBox1.Image = bmp;

            x *= 2500;
            y *= 2500;

            serialPort1.Write("motorrun 0 " + x.ToString("0") + "\n");
            serialPort1.Write("motorrun 1 " + y.ToString("0") + "\n");

            Text = x.ToString("+0000;-0000") + " " + y.ToString("+0000;-0000");
        }

        private void Form1_Load(object sender, EventArgs e) {
            serialPort1.Open();
        }

        private void Form1_FormClosed(object sender, FormClosedEventArgs e) {
            serialPort1.Close();
        }
    }
}

2015年7月9日木曜日

イリジウムフレアを撮ってみた



イリジウム7のフレアを撮ってみました。流れ星じゃないです。
上はK-5、下はα7sで撮影しています。α7sは動画で撮影した後に静止画として書き出し、比較明合成を行いました。
K-5は換算27mmで撮影してトリミングです。α7sは55mで撮影してトリミングせず。若干狙いが外れてました。

イリジウムフレアは確かに輝度が変化して面白いですが、それでも「あ~ なんか光ってるな~」程度です 見てて楽しいか・面白いかどうかは人それぞれですね。。

2015年7月3日金曜日

C#でシリアルポートのデバイス名を獲得する

C#のSystem.IO.Ports.GetPortNames()ではCOMxというそっけない文字列しか獲得できない
あんまり不便してないのだけど、試しにデバイス名を獲得してみた

C#でCOMポート番号とシリアル接続機器名を同時に取得する方法 - 真実の楽譜(フルスコア)というページを参考にした 手っ取り早く使いたいならこのページでどうぞ

さて、とりあえず試してみたソースコード

/*
 * 参考にしたページ : http://truthfullscore.hatenablog.com/entry/2014/01/10/180608
 */

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Management;

namespace ShowSerialPortName {
    class Program {
        public static void Main() {

            Stopwatch sw = new Stopwatch();
            sw.Start();
            string[] ports = GetDeviceNames();
            sw.Stop();

            Array.Sort(ports, (a, b) => (DeviceNameToIndex(a) - DeviceNameToIndex(b)));

            for (int i = 0; i < ports.Length; i++) {
                Console.WriteLine(ports[i]);
            }

            Console.WriteLine((sw.ElapsedMilliseconds * 0.001).ToString() + "sec");
            Console.ReadLine();
        }

        public static int DeviceNameToIndex(string DeviceName) {
            System.Text.RegularExpressions.Regex check = new System.Text.RegularExpressions.Regex("(COM[1-9][0-9]?[0-9]?)");

            int i = DeviceName.LastIndexOf('(');

            if (i == -1) {
                return (0);
            }

            string str = DeviceName.Substring(i);

            if (!check.IsMatch(str)) {
                return (0);
            }

            if (str.Length <= 5) {
                return (0);
            }

            int index;
            if (!int.TryParse(str.Substring(4, str.Length - 5), out index)) {
                return (0);
            }

            return (index);
        }

        public static string[] GetDeviceNames() {
            List<string> deviceNameList = new List<string>();
            System.Text.RegularExpressions.Regex check = new System.Text.RegularExpressions.Regex("(COM[1-9][0-9]?[0-9]?)");

            ManagementClass mcPnPEntity = new ManagementClass("Win32_PnPEntity");
            ManagementObjectCollection manageObjCol = mcPnPEntity.GetInstances();

            foreach (ManagementObject manageObj in manageObjCol) {
                string name = manageObj.GetPropertyValue("Name") as string;

                if (check.IsMatch(name)) {
                    deviceNameList.Add(name);
                }
            }

            return (deviceNameList.ToArray());
        }
    }
}

僕の環境ではこうなった
たった5個のシリアルポート表を入手するだけで約0.3秒もかかる。これは200以上もあるデバイスすべてに総当りで探しているので仕方ないといえば仕方ないが。PCによってはもっと時間がかかる場合もあるだろうし、きれいなPCならもっと早いかもしれない。が、どちらにしろGetPortNamesよりははるかに長いだろう。GUIフォームで使用する場合はGUIスレッドをブロックしないような方法を考える必要がありそうだ。

それとデバイス名の最後にインデックスが表示されるのはあんまり好かない。LastIndexOfで(COMx)を切り出して前に移動する という処理をしたほうが読みやすいと思う。ただこれは僕がCOMポートのテーブルを把握しているからなので、どのポート番号がどのデバイスに接続されているかわからない場合はCOMx表示がどこにあっても関係ないかも?。