Home -> 雑用 -> 雑用メモ -> [5. C#関連の糞雑魚メモ]
2015/03/18 作成
2015/03/19 更新
2015/04/02 更新
2015/04/09 更新
2015/04/10 更新
2015/04/12 更新
2015/05/06 更新
2015/05/08 更新
2015/05/12 更新
2015/08/12 更新
2015/08/25 更新
2015/09/28 更新
2015/10/26 更新
2015/11/03 更新
2016/02/07 更新
2016/02/17 更新
2016/03/18 更新
一切推敲していない糞文章故、大変読み難い代物となっております。
C#関連の雑多なメモを集約。定期的に更新というか追記していくつもり。
糞記事を量産するなと言われてもそんなの知らん。これは備忘録、覚書だから中身はユルユルのスカスカ。
Windows8.1・Visual Studio Community 2013を使用。
Community版VS公開時にMSの戦略にまんまと乗っかる形でC#を独学で使い始めてみたらこれが結構いける。
今までPHP+JSでできないことは諦めていた。今はWindowsのFormアプリケーションもPHPだけで作れる便利な時代。
故にPHPだけで数年間騙し騙し過ごしてきたがやっぱり不便なもんは不便なんでCommunity版VS公開はいい切っ掛けとなった。
.NET依存でWindows本位ではあるけどもPHPを無理矢理仮想鯖で動かすというアレなやつよりは王道に近いかなーと思われる。
リファレンスも充実してるし困ったらググれば大体解決する、それがC#。
そんな中でも特に躓いたり、謎の感動を覚えたり、無駄に苦労した末に時間の無駄と帰してしまったことをダラダラ記す。
WebBrowserコントロール内でGoogleマップを開いたところ旧バージョンで開かれ、「使っているIEのバージョンが古い」みたいな感じの
警告文みたいなのが表示された。で、調べてみたらこういうことらしい。
という訳でレジストリを弄らなければいけない模様。糞面倒。
「HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_BROWSER_EMULATION
」にexe名で
DWORD値のキーを作る。上記参考ページだとパスが「HKEY_CURRENT_USER\SOFTWARE\...
」(大文字)になってますが同意です。
でもって値がIEのバージョンに対応するらしい。ググってみるとみんな「8000
」とか「9999
」とか入れてるけど
じゃあIE11は指定できんのか、ということで調べたらMS曰く「11000
」でOKみたい。
一応書いておくと、アプリケーションAppNameのデバッグ時には「WindowsFormsApplication_AppName.exe
」ではなくて
「WindowsFormsApplication_AppName.vshost.exe
」が実行されるので、そっちのキーも作らないといけない。
これでWebBrowserコントロールで正しくGoogleマップが表示できるようになった。
どうでもいいけどexe名をレジストリに登録ってどうよ。何たる糞っぷり。
こんなもんプログラム内から設定できるようにしろや、と。Microsoftどうにかしろ。
(2015/03/18)
さっきの参考ページの続きに書いてあったからやってみる。今度もさっきの要領でexe名のキーを作る。
場所は「HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_GPU_RENDERING
」。
フルで書いたけど「FEATURE_BROWSER_EMULATION
」の部分が変わっただけだから場所はほぼ同じ。
値はDWORD値で有効/無効がそれぞれ1
/0
に対応している。2進数だけどMaxが1だから16進でも同じ。詳細は公式に書いてある。
偉そうに書いておきながら有効になった実感なし。GoogleマップってGPUレンダリングするんじゃないの?ワカラン。
今のところ特に支障もないので放置することにした。
(2015/03/18)
C#用の某TwitterライブラリをNugetからインストールしたら一緒に入ってたJson.NET。
そこで存在を知ってから愛用しているのだが読み込むjsonに合ったクラスを作っておかないといけないのは少々面倒。
手動でチマチマやってたけどやっぱ自動でやりたい。と言う訳で探したら一瞬で出てきた。jsonを突っ込むだけでクラスが出てくる。神。
因みに、C#というか.NETでjsonを扱うためのライブラリには他にDynamicJsonとかいうのもあるみたい。
JsonReaderWriterFactoryのラッパということはこっちの方が早かったりするのかな?時間があったら使ってみたい。
そういえばJsonReaderWriterFactoryって1回も使ったことない。
jsonをXmlDictionaryReaderやらXmlDictionaryWriterやらに持ってくらしいけどようわからん。なんでわざわざxmlにすんねん。
(2015/03/18)
ログの表示といえば普通ただのtextBoxを使うものだが、エラーがあったら赤字で表示したいとか、
長ーい処理が終了したら緑で表示したいとか、そういう要求を満たそうとするとRichTextBoxを使うことになる。
でもこれが中々の曲者で、中のテキストが溜まるほどに1回のAppendText()
に費やす時間が次第に長くなっていくのである。
数千行か数万行か分からないが、それくらいになると1行AppendText()
するだけで数秒~数十秒かかったりする。
(一応書いておくがミリ秒ではなく秒である。)
さすがにこれはおかしいと思って調べてみたら似たような現象が他でも再現可能なようで以下の記事を発見。少々古いが参考になる。
つまり、richTextBox.AppendText()
するとrichTextBox.Rtf
がその都度書き換えられ、そちらの処理に時間がかかっているということらしい。
確かに色文字を使いまくっていると色テーブルの整理だけでも古いRtfを走査する訳だから時間がかかるのも頷ける。
ならば使用する色が限定されているなら自分で色テーブルを最初から用意しておいてRtfの構築も自分でやってしまおうというのが高速化の考え方。
そこでこれをやってみた。Wordが生成する.rtfとは違ってrichTextBox内部のRtfはフォーマットが単純なので人力でもrtfを生成できる。
class Form1
の内部クラスとしてclass RichTextBoxWriter
なんてのを用意してrtf生成に関する情報を保持するようにした。
色テーブルは最初から作っておいて色の指定はテーブルのインデックスで行うようにした。
で、RichTextBoxWriter.Append(string s, int colornum)
みたいな感じで追加するテキストと色番号を指定してテキストを追加する。
完全なrtfが欲しい時にはRichTextBoxWriter.GetRtfString()
みたいな関数で得られるようにしてみた。
それで、richTextBox.Rtf
を更新したいときにはそれをそのまま代入すればいいかなーと。今思えば安直だった。
AppendText()
だと自動的に最下部までスクロールしてくれるが、rtfを直に更新した場合はそれを自分でやらなければいけない。
しかしそれがどうも上手くいかない。
実装した手順は、richTextBox.SelectionStart = richTextBox.Text.Length;
でキャレットを最後尾まで動かし、richTextBox.Focus();richTextBox.ScrollToCaret();
でキャレット位置までスクロールするというもの。
これをRtf更新の度に行うとその都度一瞬ではあるがスクロール前のログが見えてしまう。
スクロールは一瞬で終わるけど、画面がものすごくチラチラするのでこの方法では使い物にならない。
色々めんどくさくなって結局普通のtextBoxに落ち着いた。また暇があったら弄ってみたい。
(2015/03/19)
FiddlerはHTTPSの通信もちゃんと捕まえてくれる。じゃあFiddlerCoreはどうかというと、初期状態ではキャプチャしてくれないっぽい。
なら設定項目があるはずだと調べてみたらなんか出てきた。
つまりはスタートアップ時にDecryptSSL
のフラグを立てておけばいいそうだ。なるほどなるほど。早速やってみる。
すると何やらメッセージボックスで警告が出てきた。下のページの「Security Concerns」のところのダイアログと同じものが出てきた。
つまりは、「DO_NOT_TRUST_FiddlerRoot」っていうオレオレ証明書を追加しろということらしい。
「信用するな」って書いてある証明書を信用しろという中々おもしろい要求である。何だか心配になってくるが迷っても仕方がない。
別にPCがぶっ壊れる訳でもあるまいし、と思いダイアログメッセージに言われるがままにオレオレ証明書を追加する。
やったぜこれでHTTPSも覗き放題だ!と思ったらHTTPSを全然捕まえてくれない。何故だろう?いろいろ調べたけどさっぱりわからん。
そんな中ついに有力な記事を発見。
これによると、「FiddlerCoreのオレオレ証明書は捨ててFiddler本体からmakecert.exeをパクってそれ使え」ということらしい。
それにしても↑のリンク先の記事、めんどくさい書き方してるよなあ。FiddlerCoreに含まれてるcertmaker.dllを使った方法を長々と
書いておいて最後の方に「実はもっと簡単な方法があります」ってオイオイ。ここまで読んだのは一体何だったんだと。
そんなこんなで上の記事は「MakeCert provides sticky Certificates and the same functionality as Fiddler」の部分だけ読めばOK。
記事通りにやればめでたくC#からHTTPS覗き放題となる。
(2015/03/19)
→「5.5 本のページ綴じ部分が歪んだデジカメ画像を補正したい」
(2015/04/02)
(2015/04/09)
Windows7あたりから追加された機能の中には割と便利な機能も有ったりする。
その一つがタスクバーのアイコン裏にプログレスバーが表示されるアレである。
Formを開いていなくても進捗が確認できるので是非とも実装したい。という訳でググったら出てきた。
なるほど。「Windows API Code Pack for Microsoft. NET Framework」なるものを使えばいいらしい。
しかし上のページにあったリンクはどちらも切れていた。公式がリンク切れってどうよ。
で、またまたググったらNuGetでお手軽導入できるっぽい。
どちらがいいのかよく分からなかったので適当に下の方をインストールしてみた。で、以下のように書く。
using Microsoft.WindowsAPICodePack;
してもいいけど。
int currentValue = 55; int maximumValue = 100; Microsoft.WindowsAPICodePack.Taskbar.TaskbarManager.Instance.SetProgressValue(currentValue, maximumValue);
注意事項が一つ。Formのコンストラクタ内では使えない。System.InvalidOperationException
が発生する。
タスクバーの更新には有効なアクティブウィンドウが1つ必要である。
テスト用コードはいつもコンストラクタに突っ込む癖だったので引っ掛かってしまった。
WindowsAPICodePackには他にも便利機能が盛沢山なのでぜひ使ってみたい。
(2015/04/10)
FiddlerはそのままだとWebSocketの中身をキャプチャしてくれない。
ググったらどうもRules→Customize Rules→ScriptEditorでルールを編集すればちゃんとキャプチャしてくれるっぽい。
でもちゃんと書いたのに表示されない。なんでや。ということで少々手間を掛けて調べた。
実はこの件の為に「実践Fiddler」なる書籍を借りてきたのだが、その96頁~「4.7 HTML5 WebSocket」には以下のような記述が。
ウェブセッションリストや要求/応答インスペクタには WebSocket メッセージは表示されません。
Fiddler は WebSocket メッセージをパースする能力を持っており、将来のバージョンの Fiddlerでは、WebSocket トラフィックを表示、変更できる新しいタイプのインスペクタが導入されます。
今の段階では、クライアントとサーバーからの WebSocket メッセージは Log タブに表示されます。
なるほど。セッションリストじゃなくてLogタブに表示されるということだった。
ちなみに、ただログを出力したいだけなら数行書くだけでも大丈夫。
ただし、それだとそのうちログが一杯になってFiddler全体を巻き込んでハングアップしてしまう。
だkら色々書いてセッションリストに表示されるようにしよう、というのが上の参考記事。
で、FiddlerCoreの場合どうすればいいのか調べたけど全然出てこない。 唯一出てきたのがこれ。
なるほど。さっきFiddlerの方で書いたルールと同じことをC#で書けば良さそう。
てなわけでサクッと書いたらちゃんとキャプチャできた。ちゃんとパースもしてある。めでたしめでたし。
(2015/04/12)
→「5.9 [C#]CAPTCHAを機械学習の力で突破したい」
(2015/05/06)
WebBrowserコントロールのDOMをC#側から弄りたい場合、どうやってアクセスするのがいいか。
思いつく方法としては恐らく以下の4つくらい。
FiddlerCoreを通すというのは直感的な方法ではあるが大袈裟すぎるのでコードを書きたくない。
そこで最初は「[4]その他の方法でどうにかしてJavaScriptを突っ込んでDOMを弄る」をやってみた。
そんなこと言ってもどうすりゃいいのさとなる訳だが実はググったら面白いものが出てきた。
ブックマークレットと同じ形式にしてUrlプロパティに突っ込めばいいんだと。
そんな馬鹿な、と思うかもしれないがちゃんと動く。
string s = "alert(location.href);";//なんかJavaScriptのコードを適当に書いておく webBrowser1.Url = new Uri("javascript:" + Uri.EscapeDataString(s));
斜め上な発想に驚くばかりである。一昔前に少し流行った「アハ体験」的なものの片鱗を感じた。
でも何かおかしい。DOMを弄った瞬間に他の要素が消えてページが真っ白になってしまう。どういうこっちゃ。
Fiddlerを無効にしても変わらなかった。もうどうしたらいいのやら。
てな訳で調べてみたけど同じ症状は全くヒットしなかった。仕方ないのでいろいろ試してたらできた。
以下の2つはダメな書き方。
string s = "document.title='a';"; webBrowser1.Url = new Uri("javascript:" + Uri.EscapeDataString(s)); //DOMに代入操作とかやるコードを生で書くと駄目。
string s = @"document.write(""<script src=\""http://example.com/test.js\"" type=\""text/javascript\""></script>"");"; webBrowser1.Url = new Uri("javascript:(function(){" + Uri.EscapeDataString(s) + "})();"); //document.write()も駄目。生で書こうがクロージャー使おうが駄目なもんはダメ。
でもって以下が正しい書き方。
string s = "document.title='a';"; webBrowser1.Url = new Uri("javascript:(function(){" + Uri.EscapeDataString(s) + "})();"); //クロージャーを使って書く。document.write()以外はこれで動く。
string s = @" var script = document.createElement( 'script' ); script.type = 'text/javascript'; script.src = ""http://example.com/test.js""; var firstScript = document.getElementsByTagName( 'script' )[ 0 ]; firstScript.parentNode.insertBefore( script, firstScript );";//ここまで全部Javascript webBrowser1.Url = new Uri("javascript:(function(){" + Uri.EscapeDataString(s) + "})();"); //要素を追加するならdocument.write()じゃなくてinsertBeforeなどを使う。
別に短いコードならURI内に全部書いちゃえばいいけど長いコードの場合はそれをやっちゃ駄目。
IEやWebBrowserコンポーネントではURLの最大長は2083文字なのでそれを超えた分はちょん切られてしまう。
自分で調べたらURLとしては2085文字まで入ったけど2文字違うだけだから影響は皆無、よって2083文字と考えておいた方がいい。
また、これも自分調べなので出典がある訳ではないが、実行できるスクリプトの長さは更に短いようなので注意が必要。
300字を超えたあたりから実行してくれなくなる。
(2015/05/08)
WebBrowserコントロールを使って簡易ブラウザ的なものを作って遊んでいたらSystem.Runtime.InteropServices.COMException
が発生。
ユーザから見て同じ条件でも発生したりしなかったりする。「例外の詳細をクリップボードに追加する」でコピペしたのが以下。
System.Runtime.InteropServices.COMException はユーザー コードによってハンドルされませんでした。 HResult=-2147024726 Message=要求されたリソースは使用中です。 (HRESULT からの例外:0x800700AA) Source=System.Windows.Forms ErrorCode=-2147024726 StackTrace: 場所 System.Windows.Forms.UnsafeNativeMethods.IWebBrowser2.Navigate2(Object& URL, Object& flags, Object& targetFrameName, Object& postData, Object& headers) 場所 System.Windows.Forms.WebBrowser.PerformNavigate2(Object& URL, Object& flags, Object& targetFrameName, Object& postData, Object& headers) 場所 System.Windows.Forms.WebBrowser.set_Url(Uri value) 場所 WindowsFormsApplication_appname.Form1.backgroundWorker1_RunWorkerCompleted(Object sender, RunWorkerCompletedEventArgs e) 場所 c:\Users\(略)\WindowsFormsApplication_appname\Form1.cs:行 331 場所 System.ComponentModel.BackgroundWorker.OnRunWorkerCompleted(RunWorkerCompletedEventArgs e) 場所 System.ComponentModel.BackgroundWorker.AsyncOperationCompleted(Object arg) InnerException:
イベントハンドラForm1.backgroundWorker1_RunWorkerCompleted()
の中でwebBrowser1.Url
にnew Uri
を代入する部分で例外が発生する。
原因が分からんのでググった。
結局のところメモリ不足かな?という感じなので適当な間を見つけてGC.Collect();
するしかなさそう。
これだけ書き足して暫く走らせてるけど今のところ問題は起きていないからこれで解決可能なのかも?
(2015/05/12)
NuGetから適当にインスコすれば何となく使えちゃう便利なCSV入出力ライブラリのCsvHelper。
そのCsvHelperを適当に使っていたらヘッダが付いたり付かなかったりして挙動がイマイチつかめない。
付けるか付けないか指定すればちゃんとその通りになるけど指定しなかったらどうなるのか…?
コードを書き始めた最初のうちはその辺りが適当だったのでヘッダは付かないものだと思い込んで処理していたら不都合が生じた。
ヘッダが付いているのに付いていないつもりでCSVを読み込むとエラーを吐いて止まってしまう。
逆にヘッダが付いていないのに付いているつもりでCSVを読み込むと1行目のレコードはヘッダとして処理されてデータとして取り込まれない。
どちらにしろ不便なので挙動をちゃんと調べてみた。
まずCSV出力時にヘッダが付くかどうか確認。標本として用いるCSVを生成するためのクラスは以下のような感じにしてみる。
時刻とそれに対応する数値を順次追記していくという想定である。
using CsvHelper.Configuration; public class CsvObject { public DateTime Time { get; set; } public long Count { get; set; } public CsvObject() { this.Time = new DateTime(); this.Count = 0; } } public sealed class CustomClassMap : CsvClassMap<CsvObject> { public override void CreateMap() { Map(m => m.Time).Index(0); // ヘッダが無いときもレコードが読み込まれるようにひとまずインデックスを指定する Map(m => m.Count).Index(1); } }
出力の方法はいろいろあるが例えば以下のような感じである。CsvWriter.WriteRecord<T>(RecordObject)
でレコードを1つだけ書き込む。
これはどんどん追記したいときなんかに使える。
using CsvHelper; using System.IO; using (FileStream fs = new FileStream(filename, FileMode.Append, FileAccess.Write, FileShare.ReadWrite)) // FileMode.Createだと…? using (StreamWriter sw = new StreamWriter(fs, Encoding.UTF8)) using (CsvWriter writer = new CsvWriter(sw)) { writer.Configuration.RegisterClassMap<CustomClassMap>(); //writer.Configuration.HasHeaderRecord = false; // これを書くか書かないか…? CsvObject LastRecordObject = new CsvObject(); LastRecordObject.Time = DateTimeNow; // 適当な日時を代入 LastRecordObject.Count = CountNow; // 適当な数値を代入 writer.WriteRecord<CsvObject>(LastRecordObject); }
こんなのもある。レコードのリストを一気に書き込む時に使える。
using (StreamWriter sw = new StreamWriter(filename, System.IO.File.Exists(filename))) using (CsvWriter writer = new CsvWriter(sw)) { writer.Configuration.RegisterClassMap<CustomClassMap>(); //writer.Configuration.HasHeaderRecord = false; // これを書くか書かないか…? // ここまでの間に List<CsvObject> list を用意しておく writer.WriteRecords(list); }
実はヘッダをどうするかは設定できる。CsvWriter
やCsvReader
ごとにtrue/false
で指定する。
CsvWriter.Configuration.HasHeaderRecord = false; // ヘッダがないCSV
最初のうちはこれを知らずに使っていたのでヘッダが出力されたりされなかったりで大変だった。
一方で読込はどんな感じかというとだいたい以下のようにして書けばおk。
単一レコードの読み込みについては今は考えないことにする(というか使ったことないし使う予定もない)。
using (FileStream fs = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) using (StreamReader sr = new StreamReader(fs, Encoding.UTF8)) using (CsvReader reader = new CsvReader(sr)) { //reader.Configuration.HasHeaderRecord = false; // これを書くか書かないか…? reader.Configuration.RegisterClassMap<CustomClassMap>(); List<CsvObject> records = reader.GetRecords<CsvObject>().ToList(); // CSVに含まれる全てのレコードを読み込む Dictionary<DateTime, long> dic = records.ToDictionary(x => x.Time, x => x.Count); }
CSV入出力の際にヘッダの存在を前提とするかどうか調べて表にしてみた。
「0」: ヘッダが存在しないことを前提として処理を行う
「1」: ヘッダが存在することを前提として処理を行う
※ … レコードを既存ファイルに追記しようとするとヘッダも追記されてしまうためCSV追記時このようなコードを書いてはいけない
※※ … 既存ファイルに追記しようとすると元のCSVが消えて上書きされてしまうためCSV追記時このようなコードを書いてはいけない
★ … CSVファイル内の1行目は無視されるのでヘッダがない場合はレコードが1つ無駄になる
★★ … ヘッダがない場合、ヘッダを無理矢理レコードとして読み込もうとするので型変換でエラーが発生する
入出力 | FileMode | HasHeaderRecord | まとめ | |||
---|---|---|---|---|---|---|
未指定 | true | false | ||||
出力 | WriteRecord | Append | 0 | 0 | 0 | ヘッダは出力されない |
Create | 0※※ | 0※※ | 0※※ | |||
FileStreamを用いない | 0 | 0 | 0 | |||
WriteRecords | Append | 1※ | 1※ | 0 | 基本的にはヘッダが出力される 出力したくなかったらHasHeaderRecordを指定する |
|
Create | 1※※ | 1※※ | 0※※ | |||
FileStreamを用いない | 1※ | 1※ | 0 | |||
入力 | GetRecords | Open | 1★ | 1★ | 0★★ | 基本的にはヘッダが存在する前提で読み込む ヘッダがないならHasHeaderRecordを指定する |
FileStreamを用いない | 1★ | 1★ | 0★★ |
パーミッションのこともあるので面倒でもFileStreamから書いた方が安全。
ヘッダをくっ付けたいならCSV作成時と追記時で条件分けしなければならない。
面倒事を避けたいならヘッダなんて最初から出力しないようにした方がいい。
(2015/08/12)
HTMLをパースしたい時に何を使うべきか。そんな内容の記事はネットの海に腐るほど転がっている。
じゃあ何でこんな項目を設けたのかということだが、コード丸写ししたら動いてくれなかったからである。
まずHtml Agility Packを試した。NuGetから楽ちんインスコできたけど雑魚コードをコピったらエラーが出ちゃう。
HtmlDocument html = new HtmlDocument(); using(WebClient wc = new WebClient()) using(Stream s = wc.OpenRead(url)) { html.Load(s); }
仕方がないので断念。でも後から調べたら原因が分かった。
HtmlDocument
はてっきりSystem.Windows.Forms.HtmlDocument
だと思ってコード書いたけど違うらしい。
mshtml.HTMLDocument
かと思ったらこれも違って正しくはHtmlAgilityPack.HtmlDocument
らしい。
いくつも同じ名前付けやがって、フザケンナよ!
何はともあれ次に試したのがSGMLReader。こっちもNuGetで楽々インスコ。
コードもさっきと違って迷うところがなく快適。こんな感じのやつを作っておくといろいろ捗る。
static XDocument ParseHtml(string url) { XDocument xml; using (SgmlReader sgml = new SgmlReader { Href = url }) { sgml.IgnoreDtd = true; // 「これが無いと例外で死ぬ」…らしい xml = XDocument.Load(sgml); } return xml; }
で、こんな感じで抽出ができちゃったりして面白いね、という話でした。
XDocument xml = ParseHtml(url); XNamespace ns = xml.Root.Name.Namespace; XElement parentDiv = xml.Descendants(ns + "div") .Where(ul => ul.Attribute("class") != null && ul.Attribute("class").Value == "thumbnail"); XElement childDiv = parentDiv.Element(ns + "div"); var images = childDiv.Descendants(ns + "img"); foreach (XElement image in images) { string ThumbnailUrl = image.Attribute("src").Value; Debug.WriteLine(ThumbnailUrl); }
(2015/08/25)
→「5.14 [C#]特殊な木構造のデータ管理と図への描画」
(2015/09/28)
こんな記事は探せばいくらでもあるが、今まで冗長な書き方をしていたので。
例えば以下のようなクラスを用意してリストを表示したいとする。
[Serializable] public partial class Person { public int Id { get; set; } public string Name { get; set; } public int Score { get; set; } public Person() { this.Id = 0; this.Name = ""; this.Score = 0; } }
そんな時、今までは以下のような感じで書いていた。とりあえず動くのでいつもこんな感じで書いてきた。
事前にCellContextMenuStripNeeded
イベントハンドラを作ってDataGridView
と関連付けておく。
public partial class Form1 : Form { public BindingList<Person> BindingListMembers = new BindingList<Person>(); public int iRowIndex = 0; // DataGridViewの右クリックメニュー用インデックス public Form1() { InitializeComponent(); this.dataGridView1.DataSource = this.BindingListMembers; this.dataGridView1.Refresh(); this.dataGridView1.Columns["Id"].HeaderText = "番号"; this.dataGridView1.Columns["Id"].Width = 80; this.dataGridView1.Columns["Name"].HeaderText = "名前"; this.dataGridView1.Columns["Name"].Width = 200; this.dataGridView1.Columns["Score"].HeaderText = "得点"; this.dataGridView1.Columns["Score"].Width = 80; } private void dataGridView1_CellContextMenuStripNeeded(object sender, DataGridViewCellContextMenuStripNeededEventArgs e) { if (e.RowIndex >= 0 && e.ColumnIndex >= 0) { e.ContextMenuStrip = contextMenuStrip1; this.iRowIndex = e.RowIndex; } } // 右クリックメニューで「この人の情報を表示」が選択されたとき private void この人の情報を表示ToolStripMenuItem_Click(object sender, EventArgs e) { Person SelectedPerson = this.BindingListMembers[this.iRowIndex]; // ここまで来たら情報表示なり何なり好きにする } // 右クリックメニューで「この人を削除」が選択されたとき private void この人を削除ToolStripMenuItem_Click(object sender, EventArgs e) { Person SelectedPerson = this.BindingListMembers[this.iRowIndex]; if(MessageBox.Show("[" + SelectedPerson.Name + "]の情報を削除しますか?", "情報の削除", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2) == DialogResult.Yes) { this.BindingListMembers.RemoveAt(this.iRowIndex); // 削除時はインデックスがそのまま使える dataGridView1.Refresh(); } } }
ところが、このままだとDataGridView
が増えるたびにint iRowIndex
のような邪魔者が増えてしまう。
コードも冗長なので今までDataGridView
へのContextMenuStrip
関連付けは敬遠してきた。
しかし、少し調べたら以下のような感じで割ときれいにまとめることもできるようで、こっちだったら分かりやすそう。
public partial class Form1 : Form { public BindingList<Person> BindingListMembers = new BindingList<Person>(); public Form1() { InitializeComponent(); this.dataGridView1.DataSource = this.BindingListMembers; this.dataGridView1.Refresh(); this.dataGridView1.Columns["Id"].HeaderText = "番号"; this.dataGridView1.Columns["Id"].Width = 80; this.dataGridView1.Columns["Name"].HeaderText = "名前"; this.dataGridView1.Columns["Name"].Width = 200; this.dataGridView1.Columns["Score"].HeaderText = "得点"; this.dataGridView1.Columns["Score"].Width = 80; } private void dataGridView1_CellContextMenuStripNeeded(object sender, DataGridViewCellContextMenuStripNeededEventArgs e) { if (e.RowIndex < 0 || e.ColumnIndex < 0) { return; } e.ContextMenuStrip = this.contextMenuStrip1; DataGridView d= (DataGridView)sender; d.CurrentCell = d[e.ColumnIndex, e.RowIndex]; } private void この人の情報を表示ToolStripMenuItem_Click(object sender, EventArgs e) { Person SelectedPerson = (Person)this.dataGridView1.CurrentRow.DataBoundItem; // ここまで来たら情報表示なり何なり好きにする } private void この人を削除ToolStripMenuItem_Click(object sender, EventArgs e) { Person SelectedPerson = (Person)this.dataGridView1.CurrentRow.DataBoundItem; if(MessageBox.Show("[" + SelectedPerson.Name + "]の情報を削除しますか?", "情報の削除", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2) == DialogResult.Yes) { this.dataGridView1.Rows.Remove(this.dataGridView1.CurrentRow); // BindingList を使っているならこんな感じでおk } } }
たまにList
とBindingList
とでどちらを使っているのかあやふやになって毎回適当に誤魔化してきたが、
BindingList
ならDataGridView
側を弄るだけでBindingList
側も勝手に変わってくれる。
ところがどっこい、まだまだ改善の余地があって、BindingSource
を使うともっといい感じにやってくれる。
ちょっと下ごしらえが面倒だがこれさえ乗り越えれば列の幅・名前設定やthis.dataGridView1.Refresh();
が不要になる。
BindingList<Person>
をあらかじめコード上で作成しておくdataGridView1
のDataSource
にさっき追加したデータソースを指定すると、BindingSource
が自動的に作成される(この場合だとpersonBindingSource
といった名前で作成される)。ここまでやると、デザイナ上のdataGridView1
にヘッダが表示される。dataGridView1
を右クリックし、[列の編集...] からヘッダの名前や幅を調整する。personBindingSource.DataSource
にBindingList<Person>
を突っ込む。public partial class Form1 : Form { public BindingList<Person> BindingListMembers = new BindingList<Person>(); public Form1() { InitializeComponent(); this.personBindingSource.DataSource= this.BindingListMembers; // これだけ! } // 後は同じ // ただしリストをごっそり入れ替えたいときは注意が必要 private void newList(BindingList<Person> List) { // これが今までの書き方 this.dataGridView1.DataSource = List; this.dataGridView1.Refresh(); // こんな風に書いちゃうと駄目、DataGridViewは更新されない // DataSourceにインスタンスを突っ込む必要がある this.BindingListMembers = List; // こういう風に書けばおk DataGridViewは自動的に更新される this.personBindingSource.DataSource = List; // あるいはこういう風に書いた方が後々でコード側からBindingListを弄れて便利かも this.BindingListMembers = List; // これだけだと駄目 this.personBindingSource.DataSource = this.BindingListMembers; } }
結局のところ、長々とコードを書いているのに気づいたらもっと簡単な方法がないか調べましょう、ということであった。
(2015/10/26)
BindingList<T>
をBindingSource.DataSource
に突っ込んでおけば、リストの要素の追加・削除なんかは自動でシンクロしてくれる。
じゃあリスト内のある要素となっているオブジェクトの中身をちょっと弄ったらどうなるかというと、この時はすぐにUIには反映されない。
色々書いてあるが、こんな感じのことが書いてある。
Windowsフォームアプリケーションに貼ったDataGridView
内にPerson
オブジェクトのリストを表示したい。
コード上でのリストの変更がDataGridView
に反映されたりその逆ができるようにしたい。
BindingSource
を使えばDataGridView
と簡単に双方向連携できるという風に解釈してるだけど、
データバインドってこういう風に書けばいいのかな?//擬似コード BindingSource.DataSource = IBindingList<Person>それともこんな感じ?BindingSource.DataSource = IList<Person>これって何が違うの? どっちの方法でもリストを変更したらDataGridView
に反映されるの?
もしBindingList
じゃないと駄目ってなると依存関係の都合上よろしくないんだけどこの辺どうにかなる?
Microsoftは「BindingList
使う代わりにBindingSource
使っとけばおk」って言ってるけど…
BindingList
使っとけばコードからの変更はコントロールにすぐ反映されるよ。
BindingList
はリストが変更されたら特定のイベントを発生させるけど普通のリストはそうなってない。
普通のリストを使ったら、コントロールを通した変更やBindingSource
を通した変更はすぐに反映されるように見えるけど
コード上のリストは直接は変更されないよ。
BindingSource
使うならBindingList
使わなくても大丈夫とか言ってる人もいてゴチャゴチャするけど、
たぶん君の言いたいことはBindingSource
を通してリストを変更した時の話だよね。
IList<Person>
にバインドすると一方通行のバインディングしかできないよ。
つまりこの場合、コードからのリスト自体やリストの要素への変更はUIには反映されないんだよね。
(管理人注:DataGridView
上の編集はリストに反映されるのかな?)
BindingList
かBindingSource
を使っとけば双方向連携できるけど、
Person
クラスはINotifyPropertyChanged
をサポートしてないと駄目だよ。
でないと、リストの要素の追加・削除は同期してくれるけどリストの要素自体に変更が加えられたときにそれがUIに反映されないよ。
System.Windows.Forms
に依存させたくなかったら、代わりにObservableCollection<Person>
ってのを使うこともできるよ。
これなら必要に応じた変更通知をしてくれるし、同期もできるよ。
.NET 4 を使っていない限り、ObservableCollection
使うと依存関係が更に厄介になっちゃうよ。
こいつはWPFアセンブリ(WindowsBase)の中で定義されてるからね。.NET 4 ではSystem.dllに移ったけど。
調べてみたらObservableCollection
を使った場合でもINotifyPropertyChanged
を実装する必要があるらしい。
だったらもうBindingList
でいいじゃんって思うのでそのままこっちで書き進める。
総括するとPerson
クラスにINotifyPropertyChanged
を実装すればおkということになるのだが、
こいつが真面目にやろうとするとクソ面倒な代物で、楽に書くために色々なものが出ているようだ。
その一つとしてやたら名前が挙がっているのが「Notify Property Weaver」という Visual Studio の拡張機能。
ところが、こいつは既に削除されてしまっている。作者ページから辿ると「PropertyChanged.Fody」というパッケージが見つかるので、これを使う。
導入はNuGetが全部やってくれるから特に難しいことはないので細かいことは書かない。
コードも以下のように書き加えてやるだけで残りは全部いい感じにやってくれる。INotifyPropertyChanged
のことを考える必要は全くない。
[ImplementPropertyChanged] [Serializable] public partial class Person { // 中身は今までと全く同じ、[ImplementPropertyChanged] を書き加えただけ public int Id { get; set; } public string Name { get; set; } public int Score { get; set; } public Person() { this.Id = 0; this.Name = ""; this.Score = 0; } }
以前にも扱ったSGMLReader。中々融通の利くこいつは今まで散々使い込んできたが、特にエラーが出たりということはなかった。
ところが最近、どう頑張ってもエラーとなるページが出現。どことは言わないが、cgi出力のかなり普通のページ。
それも「http://example.com/test/201510/6」は正常に読み込めて「http://example.com/test/201510/7」だとエラーが出る、といった感じ。
HTMLの構造も文字コードもヘッダも殆ど同じなのに、なぜか特定のページだけ「System.ArgumentException」が投げられる。
追加情報で「空白以外の文字をコンテンツに追加できません」などといわれる。
static XDocument ParseHtml(string url) { XDocument xml; using (SgmlReader sgml = new SgmlReader { Href = url }) { sgml.IgnoreDtd = true; xml = XDocument.Load(sgml); // ここで System.ArgumentException が投げられる } return xml; }
このエラーは日本語で検索しても殆ど情報がなく、英語版の追加情報「Non white space characters cannot be added to content.」で検索するといろいろ出てくる。
ざっくりといえばXMLの構造が不正だと投げられるエラーということらしい。
そもそもXMLとして構造が不完全なHTMLをXMLとして読み込めるようにするのがSGMLReaderな訳で、こんなエラーを出されては困る。
検索をすれども同じ状況に関する情報は皆無で、コードを思いのまま書き換えたりしてみたが改善する兆しは無し。
そんなこんなで試行錯誤すること約3時間、ようやく正しく読み込めるようになった。
static XDocument ParseHtml(string url, Encoding enc) { XDocument xml; using (WebClient wc = new WebClient()) { wc.Encoding = enc; // 文字コードをユーザに指定させる string s = wc.DownloadString(url); // 文字列として先に全て読み込む using (StringReader sr = new StringReader(s)) // 文字列をストリームとしてSgmlReaderに渡すためにStringReaderを使う using (SgmlReader sgml = new SgmlReader()) { sgml.IgnoreDtd = true; sgml.InputStream = sr; // ここでさっきのStringReaderを指定する xml = XDocument.Load(sgml); } } return xml; }
結局原因の詳細は不明なままだが動くようになったので、今のところはこれで良しということで。
文字コードが事前にわかる場合はこっちのオーバーロード使えばおk。
(2015/11/03)
BackgroundWorkerでゴリゴリやっていた時に比べてTaskAsyncが便利なので乱用して以下のようなコードを書いたとする。
// ログ出力用 private void WriteLog(string s) { this.WriteDate(); this.textBoxLog.AppendText(s + "\r\n"); } private void writeErrorLog(string s) { this.WriteDate(); this.textBoxLog.AppendText("エラー: " + s + "\r\n"); } private void WriteDate() { this.textBoxLog.Focus(); this.textBoxLog.AppendText("[" + DateTime.Now.ToString() + "] "); } // 画像ダウンロード用 private async void button1_Click(object sender, EventArgs e) { this.button1.Enabled = false; this.button1.Text = "処理中..."; WebClient wc = new WebClient(); string UrlBase = "http://nsmr-hryk.koroshitai.jp/img/"; string Url; string FileName; int minNum = 1; int maxNum = 10000; for (int i = minNum; i <= maxNum; i++) { FileName = i.ToString("00000") + ".jpg"; Url = UrlBase + FileName; if (!File.Exists(FileName)) { await Task.Delay(1000); this.WriteLog("取得開始: " + FileName); try { await wc.DownloadFileTaskAsync(Url, FileName); this.WriteLog("取得完了"); } catch (Exception ex) { this.writeErrorLog("取得失敗: " + ex.Message); } } } this.WriteLog("処理完了"); this.button1.Text = "処理開始"; this.button1.Enabled = true; }
button1を押したら特定の場所から1万枚の画像をダウンロードする、といった感じで書いたつもり。
動作の内容はフォーム内のテキストボックスにログを表示することでユーザに示す。
手元に画像がまだダウンロードされていない段階では約1秒おきに画像を"00001.jpg"から順番に保存していく、という内容。
wc.DownloadFileTaskAsync
を使っているのでダウンロード中もUIは固まらない、というのを期待して書いたとしよう。
で、手元に画像が既に保存されている段階ではどうなるのかというと、1万枚の画像があるかどうか1枚ずつ確かめているので
それだけでも結構な時間が掛かる。Task.Delay
を間に挟まないのでその間はずっとUIが固まった状態になる。
しかも2~3秒程度のフリーズならまだしも、この動作内容だと60秒以上掛かってしまうのでVisualStudioからお叱りを受けてしまう。
(先にフォルダ内のファイル一覧を取得すればいいじゃんとかそういうのか今回は関係ないので気にしないでくださいね。)
じゃあどうにかしないといけないよね、って事になる訳だが、本質的にどうしても動作に時間が掛かる場合はやっぱり別スレッドに分けるしかない。
BackgroundWorkerでもいいけど、コードがあちこちに分散するのは面倒って考えるとやっぱりこうするしかない。
// ログ出力用に以下を追加 // 別スレッドからUIスレッドにアクセスするためにデリゲートを用意しておく // 引数は2つ以上でもおk、objectっぽくないけどintやboolもおk public delegate void WriteLogDelegate(string Message, bool IsSuccess); public void WriteLogMethod(string Message, bool IsSuccess) { if (IsSuccess) { this.WriteLog(Message); } else { this.writeErrorLog(Message); } } // 画像ダウンロード用 private async void button1_Click(object sender, EventArgs e) { this.button1.Enabled = false; this.button1.Text = "処理中..."; await Task.Run(() => { WebClient wc = new WebClient(); string UrlBase = "http://nsmr-hryk.koroshitai.jp/img/"; string Url; string FileName; int minNum = 1; int maxNum = 10000; for (int i = minNum; i <= maxNum; i++) { FileName = i.ToString("00000") + ".jpg"; Url = UrlBase + FileName; if (!File.Exists(FileName)) { Thread.Sleep(1000); // Task.Delayから書き換え Invoke(new WriteLogDelegate(WriteLogMethod), new object[] { "取得開始: " + FileName, true }); try { wc.DownloadFile(Url, FileName); // ここではawaitとか使ってない Invoke(new WriteLogDelegate(WriteLogMethod), new object[] { "取得完了", true }); } catch (Exception ex) { Invoke(new WriteLogDelegate(WriteLogMethod), new object[] { "取得失敗: " + ex.Message, false }); } } } Invoke(new WriteLogDelegate(WriteLogMethod), new object[] { "処理完了", true }); }); this.button1.Text = "処理開始"; this.button1.Enabled = true; }
ちなみに、Task.Run
の中にawaitが沢山あって書き換えが面倒だという場合には以下のようにすることもできるようだ。
動作の効率という面では微妙と言わざるを得ないが、所詮は.NETなのでこんなもん。
// ここにasyncを追加しておけば await Task.Run(async () => { // Task.Runの中でもawaitが使える(でもどちみちUIスレッドから切り離されているので見た目は一緒) await Task.Delay(1000); await wc.DownloadFileTaskAsync(Url, FileName); });
(2016/02/07)
(2016/02/17)
設定を保存したいときとかに多用してきたXmlSerializer
。例えば以下のような感じ。
昔からあるせいか、そこらじゅうで紹介されている。分かりやすいし。
private BindingList<Person> Persons; // Personクラスのインスタンスたち。これを保存したい。 // Serialize private void 設定を保存ToolStripMenuItem_Click(object sender, EventArgs e) // MenuStripから「設定を保存」を選択したのちに { if (this.saveFileDialog1.ShowDialog() != DialogResult.OK) // ファイル名を確定したら { return; } Type[] et = new Type[] { typeof(Person) }; XmlSerializer serializer = new XmlSerializer(typeof(BindingList<Person>), et); using (StreamWriter sw = new StreamWriter(this.saveFileDialog1.FileName, false, Encoding.UTF8)) // MemoryStreamとかでもOK { serializer.Serialize(sw, this.Persons); // ファイルに書き込む。 } } // Deserialize private void 設定を開くToolStripMenuItem_Click(object sender, EventArgs e) // MenuStripから「設定を開く」を選択したのちに { if (this.openFileDialog1.ShowDialog() != DialogResult.OK) // ファイル名を確定したら { return; } Type[] et = new Type[] { typeof(Person) }; XmlSerializer serializer = new XmlSerializer(typeof(BindingList<Person>), et); using (StreamReader sr = new StreamReader(this.openFileDialog1.FileName, Encoding.UTF8)) { var doc = new XmlDocument(); doc.PreserveWhitespace = true; // こいつを噛ませないと読み込み時に改行が消えちゃうので注意 doc.Load(sr); using (XmlNodeReader reader = new XmlNodeReader(doc.DocumentElement)) { this.Persons = (BindingList<Person>)serializer.Deserialize(reader); // ファイルを読み込む。 this.personBindingSource.DataSource = this.Persons; // 新しいインスタンスを突っ込むとバインド先UIも更新される } } }
StreamWriter
周辺は少し書き換えるだけで他のストリームが使える。
あまりに便利なもんだからこれで満足していたが、どうやら設定の保存というときにはDataContractSerializer
を使った方がいいらしい。
今まで知らなかったけど便利そう。
System.Runtime.Serializationを参照に追加・usingに追加する必要があるが、コードはそれほど変わらないので使い勝手はよい。
// 保存時 List<Type> et = new List<Type>() { typeof(Person) }; DataContractSerializer serializer = new DataContractSerializer(typeof(BindingList<Person>), et}); XmlWriterSettings xws = new XmlWriterSettings { Indent = true }; using (FileStream fs = new FileStream(this.saveFileDialog1.FileName, FileMode.Create)) using (XmlWriter xw = XmlWriter.Create(fs, xws)) // インデントを付けるためにXmlWriterを噛ませる { serializer.WriteObject(xw, this.Persons); xw.Flush(); }
// 読込時 List<Type> et = new List<Type>() { typeof(Person) }; DataContractSerializer serializer = new DataContractSerializer(typeof(BindingList<Person>), et}); // ここまで同じ XmlReaderSettings xrs = new XmlReaderSettings { IgnoreWhitespace = false }; using (FileStream fs = new FileStream(this.openFileDialog1.FileName, FileMode.Open)) using (XmlReader xr = XmlReader.Create(fs, xrs)) { this.Persons = (BindingList<Person>)serializer.ReadObject(xr); }
読込について、FileMode
が適当でも構わないなら以下のようにしてもおk。
// DataContractSerializer のインスタンス作るところまでは同じ XmlReaderSettings xrs = new XmlReaderSettings { IgnoreWhitespace = false }; using (XmlReader xr = XmlReader.Create(this.openFileDialog1.FileName, xrs)) { this.Persons = (BindingList<Person>)serializer.ReadObject(xr); }
またXmlReaderSettings
が適当でも構わないなら以下のようにしてもおk。
// DataContractSerializer のインスタンス作るところまでは同じ using (FileStream fs = new FileStream(this.openFileDialog1.FileName, FileMode.Open)) { this.Persons = (BindingList<Person>)serializer.ReadObject(fs); }
何でもかんでもシリアライズできると噂のDataContractSerializer
。
調子に乗ってCookieContainer
とかExpandoObject
とかを突っ込んでみたが、こいつらはダメらしい。
本当はJintからもらったExpandoObject
をImpromptuInterfaceに通して作り変えたものを保存したかった。
これならリストにしてデータバインドも簡単にできるのだが、内部で用いられる型名が実行時に生成されるために
DataContractSerializer
のコンストラクタにTypeを渡せず、保存が利かないのでは不便極まりないので
結局、同一のインターフェースを継承する保存用クラスを作成して新たなインスタンスを用意することで回避した。
(2016/03/18)
DataGridViewって便利。適当にリストを突っ込んでおけば綺麗に表示してくれる。
でもBindingList
をデータソースにしてもソートはできない。
一応、バンドするクラスにIComparable
インターフェイスを実装したりしてソートの操作を別に行うことはできる。
ところがやっぱりこの方法だとソート用のコンテキストメニューやらイベントやらを別で作らないといけないので面倒。
で、検索したら便利なものがすぐに出てきた。
動物病院とか書いてあって心配になるが、ちゃんとC#の話が書いてある。
ただ、そのままだと#region
が#endregion
で閉じられていないのでエラーが出る。
ちゃんと修正したら便利な感じ。シリアライズしてファイルに保存すると後からソート後の順番を復元できるのも便利。
ソート前の順番はどこにも記録されず消えちゃうのでそこが気にならないならおk。
下のコードは殆どコピペだが#region
の修正以外にもコンストラクタあたりを少し書き換えてある。
インスタンスを新しく作る時点で既にリストがあるならいいが、自分の場合は後からBindingList.Add()
することの方が多いので。
#region SortableBindingList 関連一式 //IBindingListのソート機能を実装したジェネリックリストクラス public class SortableBindingList<T> : BindingList<T> { public static SortableBindingList<T> ToSortableBindingList(List<T> list) { return new SortableBindingList<T>(list); } public SortableBindingList(List<T> list) : base(list) { } public SortableBindingList() : base() { } // 追加分 protected override bool SupportsSortingCore { get { return true; } } protected override void ApplySortCore(PropertyDescriptor prop, ListSortDirection direction) { List<T> items = this.Items as List<T>; if (items != null) { PropertyComparer<T> pc = new PropertyComparer<T>(prop, direction); items.Sort(pc); _isSorted = true; } else _isSorted = false; _direction = direction; _SortProperty = prop; this.OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1)); } private bool _isSorted; protected override bool IsSortedCore { get { return _isSorted; } } private ListSortDirection _direction; protected override ListSortDirection SortDirectionCore { get { return _direction; } } private PropertyDescriptor _SortProperty; protected override PropertyDescriptor SortPropertyCore { get { return _SortProperty; } } } //汎用コンペアラークラス public class PropertyComparer<T> : IComparer<T> { public PropertyComparer(PropertyDescriptor propertyName, ListSortDirection direction) { this.name = propertyName; sortDirection = (direction == ListSortDirection.Ascending) ? 1 : -1; } private PropertyDescriptor name; private int sortDirection; #region IComparer<T> メンバー public int Compare(T x, T y) { IComparable left = name.GetValue(x) as IComparable; IComparable right = name.GetValue(y) as IComparable; int result; if (left != null) result = left.CompareTo(right); else if (right == null) result = 0; else result = -1; return result * sortDirection; } #endregion } //呼び出し用拡張メソッド public static class BindingListExtensions { public static SortableBindingList<T> ToSortableBindingList<T>(this List<T> list) { return SortableBindingList<T>.ToSortableBindingList(list); } } #endregion
というか#region
って最近になって初めて存在を知ったけど便利だなー。
VisualStudioは折りたたみたいところは大体折りたためるようになっているから不要だと思う人もいるかもしれないが、
たまに気が利かなくて折り畳んだコードが勝手に展開されてしまうことがあるので、こういう機能を有効に使いたい。
(2016/03/20)
管理人Twitter: @su_te_ak/◆mmft4k9vgtL6
要望等はTwitterへ
Home -> 雑用 -> 雑用メモ -> [5. C#関連の糞雑魚メモ]
ここ以降は鯖が勝手に付加するやつです