ごまなつ Blog

楽しく働ける世界を目指して

【C#】BinaryFormatterを用いた配列のコピーはかなり遅い

結論

配列をディープコピーしたいのであれば、一つずつコピーした方が良いです。

経緯

C#では、クラスの配列をArray.Copyしたときシャローコピーになります。正確に言うと、2階層目からシャローコピーになります。1次元の配列であれば、Array.Copyでディープコピーになりますが、リストのリストといった2階層目があると2階層目からシャローコピーになります。リストのリストをディープコピーしたかったので、このような方法を用いました。

public static T DeepCopyT<T>(T target)
        {
            T result;
            BinaryFormatter b = new BinaryFormatter();
            MemoryStream mem = new MemoryStream();

            try
            {
                b.Serialize(mem, target);
                mem.Position = 0;
                result = (T)b.Deserialize(mem);
            }
            finally
            {
                mem.Dispose();
            }

            return result;
        }

BinaryFormatterでMemoryStreamをシリアライズ・デシリアライズすることで、コピーしたものを返しています。 今回私がこのメソッドに渡していた配列は、class[1000]でした。クラスはa[23].b[20].c[8]個の構造でした。これを上記のメソッドに丸ごとクラスの配列を渡すと、 何と2分かかりました。さすがに実用に耐えないということで、対策を調べるとBinaryFormatterの配列のシリアライズ・デシリアライズは遅いということが分かりました。よって

for(int i=0;i<class.Length;i++)
{
          copyArray[i] = DeepCopy.DeepCopyT(class[i]);
}

として一つずつ渡すようにすると20秒で終わりました。

【C#】DataGridViewComboBoxを一度のクリックで開きたい

結論

F4を押す処理を追加する方法が出てくるが、F4はALTと同時押しするとアプリケーション終了になるなど、ショートカットキーに使われており何か起こる可能性があるのでお勧めしない。 CellEnterイベントで、DropedDownを使おう

private void DataGridView_CellEnter(object sender, DataGridViewCellEventArgs e)
        {
            DataGridView dgv = (DataGridView)sender;
            if(dgv.Columns[e.ColumnIndex].Name == columnName 
            {
                dgv.BeginEdit(false);
                ((DataGridViewComboBoxEditingControl)dgv.EditingControl).DroppedDown = true;
            }
        }

経緯

現在作成しているフォームアプリでは、DataGridViewComboBoxColumnを追加しています。DataGridViewでコンボボックスのカラムを使うと、コンボボックスが開くまでに2回クリックしなければなりません。

  • 1回目のクリックでこのカラムがクリックされたか検知
  • 2回目のクリックでコンボボックスを開く

という処理になっているからです。これは直感に反するので、一回のクリックで開くようにする方法を調べました。すると、CellEnterイベントでSendKeys.Send("{F4}");する方法が見つかりました。これでもできたのですが、ALTを押しながらだとALT+F4となりアプリケーションが落ちるのでは?と考え実際にやってみると落ちました。F4はショートカットキーに使われるため、この方法は良くないと考え、DropedDownを使う方法を採用しました。

感想

処理は実現できたのですが、思わぬところで不具合が出ることがあるのでそこも想定したうえで採用する処理を考えていこうと思いました。

【C#】親フォームの中央に子フォームを表示したい

結論

親フォーム側で子フォームを呼び出すときに、

var frm = new Form1;
frm.Owner = this;
frm.StartPosition = FormStartPosition.CenterParent;

経緯

フォームアプリを作っており、子フォームにデータの内容を入力して、その内容を親フォームに反映するようにしていました。なにも設定せずに呼び出すと、子フォームが親フォームと離れた位置に表示されることがありました。よって、子フォーム側に

this.Location = new Point(
this.Owner.Location.X + (this.Owner.Width - this.Width) / 2,
this.Owner.Location.Y + (this.Owner.Height - this.Height) / 2);

を記載し、子フォームを表示したところ位置は親フォームの中心だったのですが、デュアルディスプレイの場合、サブディスプレイに親フォームを移動してから子フォームを表示すると、メインディスプレイに子フォームが表示されます(計算した座標は合っている)。

どちらのディスプレイに表示されているかを取得し、設定してもよかったのですが、調べていくうちに親フォーム側に設定することで実現できることが分かりました。それが

var frm = new Form1;
frm.Owner = this;
frm.StartPosition = FormStartPosition.CenterParent;

でした。呼び出す前(ShowDialogする前)に、親フォームは自分だと指定して、StartPositionを親の中心にする設定で実現できました。これなら、デュアルディスプレイでも親フォームの中心に子フォームが表示されます。

MessageBox

MessageBoxは、位置を指定するプロパティはなく、ディスプレイの中心に表示されます。位置を指定したいなら、拡張メソッドを書くしかないようです。

【C#】FolderBrowserDialogが使いにくいので、OpenFileDialog でフォルダを指定する

こちらの記事を参考にしています。 qiita.com

実現できるメソッド

void BrowseFolder()
{
    using (var ofd = new OpenFileDialog() { FileName = "SelectFolder", Filter = "Folder|.", CheckFileExists = false })
    {
        if (ofd.ShowDialog() == DialogResult.OK)
        {
            Console.WriteLine(Path.GetDirectoryName(ofd.FileName));
        }
    }
}

(参考記事から引用)

FolderBrowserDialog

  • 選択したフォルダのパスを取得
  • パス直打ちが無い
  • クイックアクセスが無い
  • フォルダを開いたときのツリー構造が見づらい

というところがあります。また、ファイル選択をする場面が多いため、OpenFileDialogのほうがイメージされやすく、その使い勝手を要求されます。よって、OpenFileDialogでフォルダ選択できるのが便利だと思われます。上記のメソッドを使えば、OpenFileDialog でフォルダのパスを取得することができます。

なぜFolderBrowserDialogはOpenFileDialogに変更要求を受けるのか

これはよく使われているのがOpenFileDialogであり、こちらをよく見慣れて・使い慣れているからだと考えます。FolderBrowserDialogにも良いところはあるのに、メジャーになっている方を求められるということではないでしょうか。

【C#】ドラッグ&ドロップが作動しないのは、権限が異なるため

ドラッグ&ドロップでファイルパスを取得する機能がWindowsアプリにあります。その機能を実装したにもかかわらず、VisualStudio上で動かしてもこの機能が動作しない。ドラッグ&ドロップできるときは、マウスカーソルが変化しますがそれすらも起こらない。調べた結果のメモです。

結論

VisualStudioをユーザ権限で開いてください(管理者権限で開かない)

経緯

以前開発していたアプリは、Windowsのサービスに追加されて常駐するものでした。Windowsのサービスに追加されるため、VisualStudioのデバッグ環境上でも毎回「VisualStudioが変更を加えることを許可しますか」のダイアログが出て煩わしかったため、VisualStudioを常に管理者権限で起動するように変更していました。

今回の状況は、権限が異なるアプリで、Windowsによって操作がブロックされています。VisualStudioが管理者権限で起動しているため、VisualStudioでのデバッグ環境で起動しているアプリは管理者権限、しかしフォルダエクスプローラはユーザ権限で開いているため、そこのフォルダからのドラッグ&ドロップは受け付けなかった、ということだと思われます。

【C#】クラスの配列要素の代入は、シャローコピーになる

C#で、xmlファイルをシリアライズ・デシリアライズするためのクラスを宣言して、その内容を変更していました。そこで、配列要素の代入なのにシャローコピーに苦しめられたのでメモです。

public class exampleclass
    {
        [System.Xml.Serialization.XmlElement("version")]
        public string version { get; set; }
        [System.Xml.Serialization.XmlElement("id")]
        public string mapid { get; set; }
        [System.Xml.Serialization.XmlElement("name")  
        public string mapname { get; set; }
        [System.Xml.Serialization.XmlElement("element")]
        public List<band> element { get; set; }
    }

public exampleclass[] Array1 = new Chmap[1000

要素名は適当ですが、このようなクラスがあり、そのクラスの2つの配列を宣言します。この後、

Array1[4] = Array1[0]
Array1[4].version = "1.99"

のように要素を別のインデックスにコピーした後、その要素を変更します。すると、Array1[0].versionも"1.99"に変更されてしまいます。

ディープコピーとシャローコピー

ディープコピー(深いコピー)とシャローコピー(浅いコピー)の違いは、簡単に言うとコピー先の変更がコピー元にも反映されるのがシャローコピー、そうでないのがディープコピーです。シャローコピーは参照をコピーしているだけなので、同じものを参照しているのでどちらも変更された後の配列を参照していることになります。

C#では、配列やリストを=で代入すると、シャローコピーになります。配列やリストは参照型です。int,doubleといった値は値型なのでディープコピーになります。

では配列やリストをどうやってディープコピーするのかですが、配列はArray.Copy,リストはコンストラクタで宣言します。

int[] a = new int[5]
int[] b = new int[5]
Array.Copy(a, b, a.Length)

var list  = new List<int>{1,2}
var list2 = new List<int>(list)

これで良いかと思われたのですが、今回のクラスの場合のような多重構造になると、直下はディープコピーになりますが、それ以降の階層ではシャローコピーになります。

参考にしたページ qiita.com

自分が採用した方法

こちらのページのメソッドを使用しました。

l-s-d.sakura.ne.jp

多重構造でもディープコピーしたい場合は自力で実装するしかないようです。MemoryStreamを用いてシリアライズ・デシリアライズしているようです。

【C#】ウィンドウの大きさに応じてパーツの大きさも変えたい

デフォルトでは、ウィンドウを最大化しても配置されたパーツはその大きさのままで、変更されません。そのため、不自然になります。ウィンドウの大きさに追従して、パーツを大きくするためには、Anchorプロパティを変更します。

Anchorプロパティ

ウィンドウが大きくなった時に、ウィンドウの端との距離を固定するかどうかを設定するプロパティです。要するに、追従させるってことですね。Top, Bottom, Left, Rightの4つを複数個から指定できます。

プロパティウィンドウで指定するときは、白ではなく灰色になっている方向が指定されています。

すべて指定していれば、大きさに追従するようになります。

パーツの上に載っているパーツは、どちらのパーツも指定しないと追従しない

例えば、Tabcontrolで3つのページを作り、それぞれにListViewを載せたとします。この時、ListViewだけにAnchorを指定しても追従しません。TabcontrolのAnchorも指定しましょう。