- 投稿日:2021-01-05T23:55:27+09:00
【メモ】C#で自作クラスのリストから重複を取り除く簡単な方法
自作クラスのリストから重複を取り除きたい
C#にはLinqのDistinctメソッドがある。
これはstring型やint型のリストには有効だが、そのままだと自作クラスでは機能しない。
IEnumerableを実装したクラスまたはIEqualityComparerの実装を作りDistinctメソッドで回すことが定石だが、もっと簡単な方法があったのでメモ φ(・・string型やint型のリストにはDistinctでOK
var array1 = new string[] { "a", "a", "b", "b" }; foreach (var x in array1.Distinct()) Console.Write(x); // ab *OK自作クラスのとき
2021/01/06: コメントを受けて修正済み
var source = new List<M> { new M { P1 = "a", P2 = "a" }, new M { P1 = "a", P2 = "a" }, new M { P1 = "b", P2 = "b" }, new M { P1 = "b", P2 = "c" } }; var array2 = source.Distinct(); foreach (var x in array2) Console.Write($"{{P1: {x.P1}, P2: {x.P2}}}"); // {P1: a, P2: a}{P1: a, P2: a}{P1: b, P2: b}{P1: b, P2: c} // {P1: a, P2: a} の重複が取り除かれていない var array3 = source .GroupBy(x => new { x.P1, x.P2 }) .Select(x => x.First()); foreach (var x in array3) Console.Write($"{{P1: {x.P1}, P2: {x.P2}}}"); // {P1: a, P2: a}{P1: b, P2: b}{P1: b, P2: c} // *OK追記(2021/01/06)
@albireo さんにもっとシンプルな方法で教えてもらいました。
C#7.3からはタプル同士を==
!=
演算子で比較できるようになったそうです。
サンプルコードはコメントを参照してください。コメントからの抜粋ですが、
GroupByを使わないと難しいのは、「重複チェックでは対象外のプロパティも重複除外後に参照したい場合」
とのことです
- 投稿日:2021-01-05T15:23:03+09:00
ExcelとかWordでクリップボードにコピーしたデータからAsciiDocの表に変換するツールをつくってみた(結合セル対応)
AsciiDocで結合セルを作るのがややこしい(といってもHTMLと同様だが)ので、AsciiDocに不慣れでも表のひな形を作成できるように、Excelの表からAsciiDocに変換するツールを作ってみた。
Excel用に作ってたらWordも小変更で対応できた。(セルの中身の処理はテキトウにつくったので、過度な期待は禁物。)補足:
Word → AsciiDocは下記で紹介されている方法でいけるらしい(自分はまだ試してない)。
脱Word、脱Markdown、asciidocでドキュメント作成する際のアレコレ - Qiitaスクリーンショット
Excel
上記の出力結果のコピー|=== .2+|a |b |c |d 2.2+|e .2+|f |g |h |i .2+|j |k |l |m 2+|n |o |p |q |===Word
上記の出力結果のコピー|=== | A | B | C 2+| D .2+| E | F | G |===C#ソースコード
- 処理対象は表1個だけです。
- ネストした表(テーブル内にテーブルがある)には対応していません。
- 書式は無視します。
ClipboardedHtmlToAsciiDocTable.csusing System; using System.Collections.Generic; using System.Drawing; using System.IO; using System.Text; using System.Text.RegularExpressions; using System.Windows.Forms; class SampleForm : Form { static readonly int ExpectedHeaderMaxLines = 20; static readonly Regex rxTableBeginTag = new Regex(@"<table(?:\s)?[^>]*>", RegexOptions.Multiline | RegexOptions.IgnoreCase); // 1 2 3 *?は最短マッチ o:pはMS office(word)対策 static readonly Regex rxTag = new Regex(@"<([a-z][a-z0-9]*|o:p)(|\s[^>]*)>|</([a-z][a-z0-9]*|o:p)>|<!--(?:.*?)-->", RegexOptions.Multiline | RegexOptions.IgnoreCase); //static readonly Regex rxTag = new Regex(@"<([a-z][a-z0-9]*)(|\s[^>]*)>|</([a-z][a-z0-9]*)>|<!--(?:.*?)-->", RegexOptions.Multiline | RegexOptions.IgnoreCase); TextBox txtAdoc; SampleForm() { Text = "HTML table(Clipborad) to AsciiDoc"; ClientSize = new Size(700, 430); var btn = new Button(){ Size = new Size(280, 25), Text = "Get AsciiDoc from Clipborad", }; btn.Click += (s,e)=>{ParseFromHtmlClipboard();}; Controls.Add(btn); var btnDbg = new Button(){ Location = new Point(300, 0), Size = new Size(220, 25), Text = "Get HTML from Clipborad(開発者用)", }; btnDbg.Click += (s,e)=>{DumpHtmlClipboard();}; Controls.Add(btnDbg); txtAdoc = new TextBox(){ Location = new Point(0,30), Size = new Size(700, 400), Text = "", Multiline = true, WordWrap = false, // 折り返し表示をしない ScrollBars = ScrollBars.Both, }; Controls.Add(txtAdoc); txtAdoc.KeyDown += (s,e)=>{ if (e.Control && e.KeyCode == Keys.A) { ((TextBox)s).SelectAll(); } }; Resize += (s,e)=>{MyResize();}; ResizeEnd += (s,e)=>{MyResize();}; } void MyResize() { int h = ClientSize.Height - txtAdoc.Top; if(h<50){h=50;} txtAdoc.Size = new Size(ClientSize.Width, h); } void ParseFromHtmlClipboard() { MemoryStream ms = GetHtmlClipboard(); if ( ms != null ) { string tmp = Parse(ms); if ( tmp != null ) { txtAdoc.Text = tmp; txtAdoc.Focus(); txtAdoc.SelectAll(); }else { txtAdoc.Text = "Parse Failed"; } } else { txtAdoc.Text = "Clipboard Load failed"; } } void DumpHtmlClipboard() { MemoryStream ms = GetHtmlClipboard(); if ( ms != null ) { string tmp = GetHtmlText(ms); if ( tmp != null ) { txtAdoc.Text = tmp; //txtAdoc.Focus(); //txtAdoc.SelectAll(); }else { txtAdoc.Text = "Parse Failed"; } } else { txtAdoc.Text = "Clipboard Load failed"; } } static MemoryStream GetHtmlClipboard() { return Clipboard.GetData("Html Format") as MemoryStream; } static string GetHtmlText(MemoryStream ms) { int startHtml = -1; int endHtml = -1; // ヘッダ情報(StartHTML, EndHTML)を取得 // StartHTML:nnnnnnnnnn // EndHTML:nnnnnnnnnn //public StreamReader(Stream stream, Encoding encoding, bool detectEncodingFromByteOrderMarks, int bufferSize, bool leaveOpen) // leaveOpen=trueで開かないと、msが閉じてしまう。 using ( var sr = new StreamReader(ms, Encoding.UTF8, true, 1024, true) ) { Regex rx = new Regex(@"^(StartHTML:|EndHTML:)([0-9]+)"); int lineCount = 0; string s; while ( (s = sr.ReadLine()) != null ) { lineCount++; Match m = rx.Match(s); if ( m.Success ) { int n = Convert.ToInt32(m.Groups[2].Value, 10); // 10進 if ( m.Groups[1].Value == "StartHTML:" ) { startHtml = n; } else { endHtml = n; } if ( startHtml >= 0 && endHtml > startHtml ) { break; } } if ( lineCount >= ExpectedHeaderMaxLines ) { break; } } } // HTML部分を取得(EndHTMLは無視) ms.Position = startHtml; using ( var sr = new StreamReader(ms, Encoding.UTF8, false) ) { return sr.ReadToEnd(); } } // stypeタグの中身を返す static string GetStyleText(string htmlText, out int endPos) { endPos = 0; int styleStartTagPos = htmlText.IndexOf("<style>"); if ( styleStartTagPos < 0 ) { return null; } styleStartTagPos += "<style>".Length; int styleEndTagPos = htmlText.IndexOf("</style>", styleStartTagPos); if ( styleEndTagPos < 0 ) { return null; } endPos = styleEndTagPos + "</style>".Length; int commentStartTagPos = htmlText.IndexOf("<!--", styleStartTagPos); if ( commentStartTagPos >= 0 ) { commentStartTagPos += "<!--".Length; } int commentEndTagPos = (commentStartTagPos<0)?-1:htmlText.IndexOf("-->", commentStartTagPos); if (commentStartTagPos >= 0 && commentEndTagPos > commentStartTagPos && commentEndTagPos < styleEndTagPos ) { // コメントタグがある場合、コメントタグを除去(コメントタグ内のみを返す) return htmlText.Substring(commentStartTagPos, commentEndTagPos - commentStartTagPos); } else { // コメントタグがない場合 return htmlText.Substring(styleStartTagPos, styleEndTagPos - styleStartTagPos); } } static int IndexOfUsingRegex(string src, int startPos, Regex rTarget, out int length) { Match m = rTarget.Match(src, startPos); if ( !m.Success ) { length = 0; return -1; } length = m.Groups[0].Length; return m.Groups[0].Index; } // tableタグ込みで返す // ネストは許容しない(検出してnullを返す) static string GetFirstTableText(string htmlText, int pos) { int len; int tableStartTagPos = IndexOfUsingRegex(htmlText, pos, rxTableBeginTag, out len); if ( tableStartTagPos < 0 ) { return null; } int tableEndTagPos = htmlText.IndexOf("</table>", tableStartTagPos+len); if ( tableEndTagPos < 0 ) { return null; } int dummy; int tmpPos = IndexOfUsingRegex(htmlText, tableStartTagPos+len, rxTableBeginTag, out dummy); if ( tmpPos>=0 && tmpPos<tableEndTagPos ) { // ネストしている(閉じタグよりも手前の位置に2つ目の開始タグを検出した) return null; } tableEndTagPos += "</table>".Length; return htmlText.Substring(tableStartTagPos, tableEndTagPos - tableStartTagPos); } //static readonly Regex rxCss = new Regex(@":;", RegexOptions.Multiline | RegexOptions.IgnoreCase); //static Dictionary<string,Dictionary<string,string>> ParseCssPart(string styleText) //{ //} // https://momdo.github.io/html/syntax.html#attributes-2 // 属性名は、制御文字、U+0020 SPACE、U+0022(")、U+0027(')、U+003E(>)、U+002F(/)、U+003D(=)、および非文字以外の1つ以上の文字で構成されなければならない。HTML構文において、外来要素に対するものでさえ、属性名は、ASCII小文字およびASCII大文字の任意の組み合わせで書かれてもよい。 // 属性値は、テキストが曖昧なアンパサンドを含めることができない追加の制限をもつ場合を除き、テキストおよび文字参照の混合物である。 // 引用符で囲まれない属性値構文 // ASCII空白文字 U+0022 QUOTATION MARK文字(")、 // U+0027 APOSTROPHE文字(')、U+003D EQUALS SIGN文字(=)、 // U+003C LESS-THAN SIGN文字(<)、U+003E GREATER-THAN SIGN文字(>)、 // またはU+0060 GRAVE ACCENT文字(`)文字を含んではならず、かつ空文字列であってはならない。 // 1 = 2 3 4 // <-------------------------------------> <--------------------------------> <-----> <-------> // <----------------------------------------------------------------> static readonly Regex rxAttr = new Regex(@"\b([^\x00-\x1F\x20\x22\x27\x2F\x3D\x3E]+)\s*(?:=\s*(?:([^\x20\x22\x27\x3C\x3D\x3E\x60]+)|'([^']*)'|\x22([^x22]*)\x22))?", RegexOptions.Multiline | RegexOptions.IgnoreCase); static Dictionary<string,string> ParseAttrs(string attrsStr) { var dict = new Dictionary<string,string>(); Match mAttr = rxAttr.Match(attrsStr); while ( mAttr.Success ) { string key = mAttr.Groups[1].Value.ToLower(); string value = ""; if ( mAttr.Groups[2].Length>0 ) { value = mAttr.Groups[2].Value; } else if(mAttr.Groups[3].Length>0) { value = mAttr.Groups[3].Value; } else if(mAttr.Groups[4].Length>0) { value = mAttr.Groups[4].Value; } else { // without "=" // do nothing } if ( !dict.ContainsKey(key) ) { dict.Add(key, value); } mAttr = mAttr.NextMatch(); } return dict; } static string EscapeContentForAdocTableCell(string s) { // Replace (string input, string replacement); s = rxTag.Replace(s, ""); // HTML全般のタグを消去 s = s.Replace("\r\n", " ") .Replace("\n", " ") .Replace("\r", " ") .Replace("\t", " ") .Replace(" ", " ") .Replace("<", "<") .Replace(">", ">") .Replace("&", "&") .Replace("|", "{VBar}"); // ADoc用 return s; } static string ParseTableToAdoc(string tableText) { var sb = new StringBuilder(); Match m = rxTag.Match(tableText); string lastStartTag = null; int lastPos = -1; int lastTdPos = -1; int currentTdCount = 0; sb.AppendLine("|==="); while (m.Success) { if ( m.Groups[1].Length > 0 ) { string tag = m.Groups[1].Value; string attrsStr = m.Groups[2].Value; lastPos = m.Groups[0].Index + m.Groups[0].Length; if ( tag == "tr" ) { currentTdCount = 0; } else if ( tag == "td" ) { lastTdPos = lastPos; if ( lastStartTag != "tr" ) { sb.Append(" "); } var attrs = ParseAttrs(attrsStr); if (attrs.ContainsKey("colspan") || attrs.ContainsKey("rowspan")){ if ( attrs.ContainsKey("colspan") ) { sb.Append(attrs["colspan"]); } if ( attrs.ContainsKey("rowspan") ) { sb.Append("."); sb.Append(attrs["rowspan"]); } sb.Append("+"); } sb.Append("|"); currentTdCount++; } lastStartTag = tag; } else if ( m.Groups[3].Length > 0 ) { string tag = m.Groups[3].Value; int tagStartPos = m.Groups[0].Index; //Console.WriteLine("</" + m.Groups[3].Value +">"); if ( tag == "tr" ) { sb.AppendLine(""); } else if ( tag == "td" ) { string s = tableText.Substring(lastTdPos, tagStartPos - lastTdPos); sb.Append(EscapeContentForAdocTableCell(s)); lastTdPos = -1; } lastPos = -1; lastStartTag = null; } m = m.NextMatch(); } sb.AppendLine("|==="); Console.WriteLine(sb.ToString()); return sb.ToString(); } static string Parse(MemoryStream ms) { // debug code { //string htmlText = File.ReadAllText("testdata_html_excel.txt"); // } end of debug code string htmlText = GetHtmlText(ms); int pos; string styleText = GetStyleText(htmlText, out pos)??""; string tableText = GetFirstTableText(htmlText, pos); return ParseTableToAdoc(tableText); } [STAThread] static void Main(string[] args) { Application.Run(new SampleForm()); } }JavaScriptに移植してWeb上に公開してみた
開発時メモ: クリップボード形式は MIMEタイプとして
text/html
を指定すると、オフセット情報とかのヘッダ情報なしの html が得られる。ネストチェックはしていない。
See the Pen TableOfHtml2AsciiDoc by kob58im (@kob58im) on CodePen.
TableOfHtml2AsciiDoc - CodePen
参考サイト
- 投稿日:2021-01-05T12:00:19+09:00
NuGetパッケージをGitHubPackagesにPushしようとして404が返ってくる場合の対処
実現したいこと
- C#でNuGetパッケージを作成する。
- GitHubでコードを管理する。
- GitHubActionsで、当該リポジトリのGitHubPackagesに配置する。
起きた事象
GitHubActions内にてNuGetパッケージをPushする。
- 環境変数
${GITHUB_TOKEN}
=${{ secrets.GITHUB_TOKEN }}
${NUGET_SOURCE}
= GitHubPackageのURL
- 例:
"https://nuget.pkg.github.com/{organization-name}/index.json"
dotnet nuget push '*.nupkg' -k ${GITHUB_TOKEN} -s ${NUGET_SOURCE} --skip-duplicate404エラーが返ってくる。
Pushing {package-name}.nupkg to 'https://nuget.pkg.github.com/{organization-name}'... PUT https://nuget.pkg.github.com/{organization-name}/ NotFound https://nuget.pkg.github.com/{organization-name}/ 377ms error: Response status code does not indicate success: 404 (Not Found).原因・対処
NuGetパッケージとなるC#プロジェクトの
.csproj
ファイルを確認します。
- RepositoryUrl
- このURLがGitHubのリポジトリURLと一致していない場合、404エラーが返ってきます。
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netstandard2.1</TargetFramework> ... <RepositoryUrl>https://github.com/{organization-name}/{repository-name}</RepositoryUrl> ... </PropertyGroup> <ItemGroup> ... </ItemGroup> </Project>備考
個人リポジトリでもOrganizationsリポジトリでも同様と思われます。
また、今回はGitHubActionsでのエラーでしたが、ローカルからCLIでPushしようとしても同様にエラーになります。NuGetパッケージを作成する場合は、PropertyGroupをきちんと記述したほうがいいですね。
- 投稿日:2021-01-05T09:48:02+09:00
C#でINIファイル操作を属性(Attribute)使ってやってみた
C#でINIファイル操作
はじめにのはじめに
2021年あけましておめでとうございます。
極力今年はこまめにコードを書いていきたく、小ネタの集まりになりますがよろしくお願いいたします。
では2021年最初のネタです。
はじめに
- App.Configを利用し環境設定を保持するのが普通?の使い方と思われます。
- ですがまだまだ以前通りの使い方をしたいユーザのニーズがありなかなかXML環境に移行するのは・・・が実情です。
- それと多くのユーザが使っていると、なぜか一部のユーザからApp.Configを参照できないケースの報告があり(記述にヌケがある?)、個人的には枯れた技術であるINIを使うケースもあります。
使い方
- 単純にWindowsAPIを使います。
- 以下のコードをクラス内で定義して呼び出すだけです。
定義は以下の通り
- ※対象を文字列のみにしています。
- 私の使い方は数値を扱うAPIは使わず、文字列を読み取ってコード側で変換しています。(深い理由もないですが。。。)
#region INIファイル操作 /// <summary> /// INIファイルから文字列の取得 /// </summary> /// <param name="lpAppName">アプリケーション名</param> /// <param name="lpKeyName">キー</param> /// <param name="lpDefault">デフォルト値</param> /// <param name="lpReturnedString">戻り値</param> /// <param name="nSize">最大文字数</param> /// <param name="lpFileName">ファイル名</param> /// <returns>読み取りバイト数</returns> [DllImport("KERNEL32.DLL")] public static extern uint GetPrivateProfileString( string lpAppName, string lpKeyName, string lpDefault, StringBuilder lpReturnedString, uint nSize, string lpFileName); /// <summary> /// INIファイルへ文字列の書き込み /// </summary> /// <param name="lpAppName">アプリケーション名</param> /// <param name="lpKeyName">キー</param> /// <param name="lpString">書き込み文字数</param> /// <param name="lpFileName">ファイル名</param> /// <returns></returns> [DllImport("KERNEL32.DLL")] public static extern uint WritePrivateProfileString( string lpAppName, string lpKeyName, string lpString, string lpFileName); #endregion使い方
以下のように使います。
string appName = @"アプリケーション名"; string keyName = @"キー名"; string defaultValue = @"戻り値のデフォルト"; StringBuilder sb = new StringBuilder(256); // 戻り値 string iniFileName = Path.Combine(Application.StartupPath,"SETTING.INI"); uint res = GetPrivateProfileString(appName, keyName, defaultValue, sb, 256, iniFileName);SETTING.INIの記述
[アプリケーション名] キー名=値
- 結果、
resに"値"のバイト数が入り、
sb.ToString()
は値
を返します。
この使い方は
- 正直に言うとこのままでは、値が増えるたびにコードを追加し続ける必要があるので面倒です。
- 同じコードを繰り返すのもなんだか・・・ですね。
属性を使ってコントロールする
- C#にはせっかくAttributeがあるので、活用します。
サンプルのINIファイルは以下の通りとします。
- ファイルのパスはテスト用の実行ファイルと同じフォルダーにあるものとします。
SETTING.INI
[app1] data1=データ1 data2=データ2 [app2] data3=データ3
- INIのデータを受け取るクラスを作ります。
/// <summary> /// 設定保存用クラス /// </summary> public class SettingClass { /// <summary> /// データ(アプリケーション名:app1 / キー名:data1) /// </summary> public string data1 { get; set; } /// <summary> /// データ(アプリケーション名:app1 / キー名:data2) /// </summary> public string data2 { get; set; } /// <summary> /// データ(アプリケーション名:app2 / キー名:data3 / デフォルト値:def3) /// </summary> public string data3 { get; set; } }
クラスとファイル名を関連付けしてみます。
まずは属性用のクラスを作成します。
/// <summary> /// INIファイル名属性 /// </summary> public class IniFileNameAttribute { /// <summary> /// ファイル名 /// </summary> public string FileName { get; set; } }
- 当然ですがこのままでは属性として認識されません。
System.Attribute
を継承します。/// <summary> /// INIファイル名属性 /// </summary> public class IniFileNameAttribute : System.Attribute { /// <summary> /// ファイル名 /// </summary> public string FileName { get; set; } }
- これがクラスや構造体で使えると宣言します。
- 宣言自体も属性で付与します。
/// <summary> /// INIファイル名属性 /// </summary> [System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Struct)] public class IniFileNameAttribute : System.Attribute { /// <summary> /// ファイル名 /// </summary> public string FileName { get; set; } }
- INIのデータを受け取るクラスに属性を付与してみます。
/// <summary> /// 設定保存用クラス /// </summary> [IniFileName(FileName = "SETTINGS.INI")] public class SettingClass { {省略}
- 属性の名称とクラス名は
Attribute
の部分が省略されています。引数名を書かなければならないため、ちょっと冗長な感じですね。。。
属性にコンストラクターをつけます。/// <summary> /// INIファイル名属性 /// </summary> [System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Struct)] public class IniFileNameAttribute : System.Attribute { /// <summary> /// ファイル名 /// </summary> public string FileName { get; set; } /// <summary> /// INIファイルに対するファイル名属性 /// </summary> /// <param name="filename">ファイル名</param> public IniFileNameAttribute(string filename) { FileName = filename; } }データ受け取りクラスも以下のようになります。
/// <summary> /// 設定保存用クラス /// </summary> [IniFileName("SETTINGS.INI")] public class SettingClass { {省略}気持ち、ですがすっきりしました。
では、要素にも属性をつけましょう。
- 要素は、プロパティやフィールドに付与できるよう属性をつけます。
- 与えるのは、アプリケーション名、キー名、デフォルト値です。
- コンストラクターではアプリケーション名とキー名のみ強制し、デフォルト値を任意にします。
/// <summary> /// INIファイルに対する属性値 /// </summary> [System.AttributeUsage(System.AttributeTargets.Property | AttributeTargets.Field)] public class IniFileAttribute : System.Attribute { /// <summary> /// アプリケーション名 /// </summary> public string Application { get; set; } /// <summary> /// キー /// </summary> public string Key { get; set; } /// <summary> /// デフォルト値 /// </summary> public string DefaultValue { get; set; } /// <summary> /// INIに対する連携用属性 /// </summary> /// <param name="application">アプリケーション名</param> /// <param name="key">キー</param> public IniFileAttribute(string application, string key) { Application = application; Key = key; DefaultValue = string.Empty; } }属性を付与した形は以下になります。
/// <summary> /// 設定保存用クラス /// </summary> [IniFileName("SETTINGS.INI")] public class SettingClass { /// <summary> /// データ(アプリケーション名:app1 / キー名:data1) /// </summary> [IniFile("app1","data1")] public string data1 { get; set; } /// <summary> /// データ(アプリケーション名:app1 / キー名:data2) /// </summary> [IniFile("app1","data2")] public string data2 { get; set; } /// <summary> /// データ(アプリケーション名:app2 / キー名:data3 / デフォルト値:def3) /// </summary> [IniFile("app2","data3", DefaultValue="def3")] public string data3 { get; set; } }こうなると、コメントは冗長な気がしてきます。
/// <summary> /// 設定保存用クラス /// </summary> [IniFileName("SETTINGS.INI")] public class SettingClass { /// <summary> /// データ1 /// </summary> [IniFile("app1","data1")] public string data1 { get; set; } /// <summary> /// データ2 /// </summary> [IniFile("app1","data2")] public string data2 { get; set; } /// <summary> /// データ3 /// </summary> [IniFile("app2","data3", DefaultValue="def3")] public string data3 { get; set; } }すっきりしました。
では、肝心な読み取り部分を作成します。
クラスが持つ属性の取得方法
- Attributeの値を取得するには、以下の手順で取得します。
// まずは型を取得 Type t = typeof(SettingClass); // 型が持つ属性を(この場合はIniFileAttribute属性)を取得 var att = (IniFileNameAttribute) Attribute.GetCustomAttribute(t, typeof(IniFileNameAttribute)); // 取得できなければ処理を抜ける if (att == null) return null; // 属性値、FileNameを取得 string iniFileName = att.FileName;
プロパティが持つ属性の取得方法
// まずは型を取得 Type t = typeof(SettingClass); // まずは型のプロパティを全て取得 var prof = t.GetProperties(); // 全て取得したプロパティをひとつづつチェックする foreach (var prop in prof) { // ----->>> 属性の有無をチェック var att2 = (IniFileAttribute) prop.GetCustomAttribute(typeof(IniFileAttribute)); // ----->>> 属性がなければ無視 if (att2 == null) continue; // ----->>> 属性からキーを得る string appName = att2.Application; string keyName = att2.Key; string defaultValue = att2.DefaultValue; // ----->>> 戻り値の準備 StringBuilder sb = new StringBuilder(256); // ----->>> INIから取得 uint res = GetPrivateProfileString(appName, keyName, defaultValue, sb, 256, iniFileName); }いかがですか?
クラスのプロパティに付与された属性の取得方法、意外と簡単ではないでしょうか。
後は、取得したプロパティの実体にデータを流し込むだけです。
- 以下は、
result
変数に対して、戻り値を設定しています。- サンプルは文字列と日付型のみを扱ってみています。
SettingClass result = new SettingClass(); // 文字列型のプロパティの場合 if (prop.PropertyType == typeof(string)) { // ----->>> 文字列の場合 prop.SetValue(result, sb.ToString()); } // 日付型のプロパティの場合 else if (prop.PropertyType == typeof(DateTime)) { // ----->>> 日付型の場合 if (DateTime.TryParse(sb.ToString(), out DateTime dmy)) prop.SetValue(result, dmy); }データを受け付けるクラスが肥大化しても、読み込みの部分をいちいち増やす必要はありません。
(扱うプロパティの型を増やせば当然コードを増やさなければなりませんが)
GetPrivateProfileString
コードを1行増やせば簡単のに、なぜこんなに手間を増やさなきゃならないの?とかいろいろご意見はあると思います。
今回はINIの話でしたが、属性を使えば相手がデータベースでも同様にできます。
(その場合はEntityFramework使えと怒られそうですが)
いろいろな使い方ができるので、属性を使ってみようかな、と思っていただけると幸いです。
完全な形は以下の通りとなります。
/// <summary> /// INIファイル操作系 /// </summary> public static class IniFileRWClass { #region INIファイル操作 /// <summary> /// INIファイルから文字列の取得 /// </summary> /// <param name="lpAppName">アプリケーション名</param> /// <param name="lpKeyName">キー</param> /// <param name="lpDefault">デフォルト値</param> /// <param name="lpReturnedString">戻り値</param> /// <param name="nSize">最大文字数</param> /// <param name="lpFileName">ファイル名</param> /// <returns>読み取りバイト数</returns> [DllImport("KERNEL32.DLL")] public static extern uint GetPrivateProfileString( string lpAppName, string lpKeyName, string lpDefault, StringBuilder lpReturnedString, uint nSize, string lpFileName); /// <summary> /// INIファイルへ文字列の書き込み /// </summary> /// <param name="lpAppName">アプリケーション名</param> /// <param name="lpKeyName">キー</param> /// <param name="lpString">書き込み文字数</param> /// <param name="lpFileName">ファイル名</param> /// <returns></returns> [DllImport("KERNEL32.DLL")] public static extern uint WritePrivateProfileString( string lpAppName, string lpKeyName, string lpString, string lpFileName); #endregion #region INI設定ファイル操作 /// <summary> /// 設定をファイルから取り込む /// </summary> /// <returns></returns> public static SettingClass ReadIniSetting() { //! ================================================================================ //! ----->>> 属性からINIファイル名を取得する(パスはEXEと同じフォルダにする) SettingClass result = new SettingClass(); Type t = typeof(SettingClass); var att = (IniFileNameAttribute) Attribute.GetCustomAttribute(t, typeof(IniFileNameAttribute)); if (att == null) return null; string iniFileName = att.FileName; string directry = Application.StartupPath; iniFileName = Path.Combine(directry, iniFileName); //! ================================================================================ //! ----->>> プロパティを検索し、属性値からアプリケーション名とキーを取得してINIファイルから値を得る var prof = t.GetProperties(); foreach (var prop in prof) { //? ----->>> 属性の有無をチェック var att2 = (IniFileAttribute) prop.GetCustomAttribute(typeof(IniFileAttribute)); if (att2 == null) continue; //? ----->>> 属性からキーを得る string appName = att2.Application; string keyName = att2.Key; string defaultValue = att2.DefaultValue; //? ----->>> 戻り値の準備 StringBuilder sb = new StringBuilder(256); //? ----->>> INIから取得 uint res = GetPrivateProfileString(appName, keyName, defaultValue, sb, 256, iniFileName); if (prop.PropertyType == typeof(string)) { //? ----->>> 文字列の場合 prop.SetValue(result, sb.ToString()); } else if (prop.PropertyType == typeof(DateTime)) { //? ----->>> 日付型の場合 if (DateTime.TryParse(sb.ToString(), out DateTime dmy)) { prop.SetValue(result, dmy); } } } //? ================================================================================ //? ----->>> 戻り値を返す return result; } /// <summary> /// 設定をファイルに書き込む /// </summary> /// <param name="data"></param> public static void WriteIniSetting(SettingClass data) { //! ================================================================================ //! ----->>> 属性からINIファイル名を取得する(パスはEXEと同じフォルダにする) Type t = typeof(SettingClass); var att = (IniFileNameAttribute)Attribute.GetCustomAttribute(t, typeof(IniFileNameAttribute)); if (att == null) return; string iniFileName = att.FileName; string directry = Application.StartupPath; iniFileName = Path.Combine(directry, iniFileName); //! ================================================================================ //! ----->>> プロパティを検索し、属性値からアプリケーション名とキーを取得してINIファイルへ値を書き込む var prof = t.GetProperties(); foreach (var prop in prof) { var att2 = (IniFileAttribute)prop.GetCustomAttribute(typeof(IniFileAttribute)); if (att2 == null) continue; //? ----->>> キーを取得 string appName = att2.Application; string keyName = att2.Key; //? ----->>> 値を文字列に変換する string Value = prop.PropertyType == typeof(DateTime) ? ((DateTime) prop.GetValue(data)).ToString("yyyy/MM/dd") : prop.GetValue(data).ToString(); //? ----->>> INIファイルに書き込む WritePrivateProfileString(appName, keyName, Value, iniFileName); } } #endregion }最後までお付き合いいただき、誠にありがとうございました。
- 投稿日:2021-01-05T09:48:02+09:00
C#でINIファイル操作で属性を使ってみた
C#でINIファイル操作
はじめにのはじめに
2021年あけましておめでとうございます。
極力今年はこまめにコードを書いていきたく、小ネタの集まりになりますがよろしくお願いいたします。
では2021年最初のネタです。
はじめに
- App.Configを利用し環境設定を保持するのが普通?の使い方と思われます。
- ですがまだまだ以前通りの使い方をしたいユーザのニーズがありなかなかXML環境に移行するのは・・・が実情です。
- それと多くのユーザが使っていると、なぜか一部のユーザからApp.Configを参照できないケースの報告があり(記述にヌケがある?)、個人的には枯れた技術であるINIを使うケースもあります。
使い方
- 単純にWindowsAPIを使います。
- 以下のコードをクラス内で定義して呼び出すだけです。
定義は以下の通り
- ※対象を文字列のみにしています。
- 私の使い方は数値を扱うAPIは使わず、文字列を読み取ってコード側で変換しています。(深い理由もないですが。。。)
#region INIファイル操作 /// <summary> /// INIファイルから文字列の取得 /// </summary> /// <param name="lpAppName">アプリケーション名</param> /// <param name="lpKeyName">キー</param> /// <param name="lpDefault">デフォルト値</param> /// <param name="lpReturnedString">戻り値</param> /// <param name="nSize">最大文字数</param> /// <param name="lpFileName">ファイル名</param> /// <returns>読み取りバイト数</returns> [DllImport("KERNEL32.DLL")] public static extern uint GetPrivateProfileString( string lpAppName, string lpKeyName, string lpDefault, StringBuilder lpReturnedString, uint nSize, string lpFileName); /// <summary> /// INIファイルへ文字列の書き込み /// </summary> /// <param name="lpAppName">アプリケーション名</param> /// <param name="lpKeyName">キー</param> /// <param name="lpString">書き込み文字数</param> /// <param name="lpFileName">ファイル名</param> /// <returns></returns> [DllImport("KERNEL32.DLL")] public static extern uint WritePrivateProfileString( string lpAppName, string lpKeyName, string lpString, string lpFileName); #endregion使い方
以下のように使います。
string appName = @"アプリケーション名"; string keyName = @"キー名"; string defaultValue = @"戻り値のデフォルト"; StringBuilder sb = new StringBuilder(256); // 戻り値 string iniFileName = Path.Combine(Application.StartupPath,"SETTING.INI"); uint res = GetPrivateProfileString(appName, keyName, defaultValue, sb, 256, iniFileName);SETTING.INIの記述
[アプリケーション名] キー名=値
- 結果、
resに"値"のバイト数が入り、
sb.ToString()
は値
を返します。
この使い方は
- 正直に言うとこのままでは、値が増えるたびにコードを追加し続ける必要があるので面倒です。
- 同じコードを繰り返すのもなんだか・・・ですね。
属性を使ってコントロールする
- C#にはせっかくAttributeがあるので、活用します。
サンプルのINIファイルは以下の通りとします。
- ファイルのパスはテスト用の実行ファイルと同じフォルダーにあるものとします。
SETTING.INI
[app1] data1=データ1 data2=データ2 [app2] data3=データ3
- INIのデータを受け取るクラスを作ります。
/// <summary> /// 設定保存用クラス /// </summary> public class SettingClass { /// <summary> /// データ(アプリケーション名:app1 / キー名:data1) /// </summary> public string data1 { get; set; } /// <summary> /// データ(アプリケーション名:app1 / キー名:data2) /// </summary> public string data2 { get; set; } /// <summary> /// データ(アプリケーション名:app2 / キー名:data3 / デフォルト値:def3) /// </summary> public string data3 { get; set; } }
クラスとファイル名を関連付けしてみます。
まずは属性用のクラスを作成します。
/// <summary> /// INIファイル名属性 /// </summary> public class IniFileNameAttribute { /// <summary> /// ファイル名 /// </summary> public string FileName { get; set; } }
- 当然ですがこのままでは属性として認識されません。
System.Attribute
を継承します。/// <summary> /// INIファイル名属性 /// </summary> public class IniFileNameAttribute : System.Attribute { /// <summary> /// ファイル名 /// </summary> public string FileName { get; set; } }
- これがクラスや構造体で使えると宣言します。
- 宣言自体も属性で付与します。
/// <summary> /// INIファイル名属性 /// </summary> [System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Struct)] public class IniFileNameAttribute : System.Attribute { /// <summary> /// ファイル名 /// </summary> public string FileName { get; set; } }
- INIのデータを受け取るクラスに属性を付与してみます。
/// <summary> /// 設定保存用クラス /// </summary> [IniFileName(FileName = "SETTINGS.INI")] public class SettingClass { {省略}
- 属性の名称とクラス名は
Attribute
の部分が省略されています。引数名を書かなければならないため、ちょっと冗長な感じですね。。。
属性にコンストラクターをつけます。/// <summary> /// INIファイル名属性 /// </summary> [System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Struct)] public class IniFileNameAttribute : System.Attribute { /// <summary> /// ファイル名 /// </summary> public string FileName { get; set; } /// <summary> /// INIファイルに対するファイル名属性 /// </summary> /// <param name="filename">ファイル名</param> public IniFileNameAttribute(string filename) { FileName = filename; } }データ受け取りクラスも以下のようになります。
/// <summary> /// 設定保存用クラス /// </summary> [IniFileName("SETTINGS.INI")] public class SettingClass { {省略}気持ち、ですがすっきりしました。
では、要素にも属性をつけましょう。
- 要素は、プロパティやフィールドに付与できるよう属性をつけます。
- 与えるのは、アプリケーション名、キー名、デフォルト値です。
- コンストラクターではアプリケーション名とキー名のみ強制し、デフォルト値を任意にします。
/// <summary> /// INIファイルに対する属性値 /// </summary> [System.AttributeUsage(System.AttributeTargets.Property | AttributeTargets.Field)] public class IniFileAttribute : System.Attribute { /// <summary> /// アプリケーション名 /// </summary> public string Application { get; set; } /// <summary> /// キー /// </summary> public string Key { get; set; } /// <summary> /// デフォルト値 /// </summary> public string DefaultValue { get; set; } /// <summary> /// INIに対する連携用属性 /// </summary> /// <param name="application">アプリケーション名</param> /// <param name="key">キー</param> public IniFileAttribute(string application, string key) { Application = application; Key = key; DefaultValue = string.Empty; } }属性を付与した形は以下になります。
/// <summary> /// 設定保存用クラス /// </summary> [IniFileName("SETTINGS.INI")] public class SettingClass { /// <summary> /// データ(アプリケーション名:app1 / キー名:data1) /// </summary> [IniFile("app1","data1")] public string data1 { get; set; } /// <summary> /// データ(アプリケーション名:app1 / キー名:data2) /// </summary> [IniFile("app1","data2")] public string data2 { get; set; } /// <summary> /// データ(アプリケーション名:app2 / キー名:data3 / デフォルト値:def3) /// </summary> [IniFile("app2","data3", DefaultValue="def3")] public string data3 { get; set; } }こうなると、コメントは冗長な気がしてきます。
/// <summary> /// 設定保存用クラス /// </summary> [IniFileName("SETTINGS.INI")] public class SettingClass { /// <summary> /// データ1 /// </summary> [IniFile("app1","data1")] public string data1 { get; set; } /// <summary> /// データ2 /// </summary> [IniFile("app1","data2")] public string data2 { get; set; } /// <summary> /// データ3 /// </summary> [IniFile("app2","data3", DefaultValue="def3")] public string data3 { get; set; } }すっきりしました。
では、肝心な読み取り部分を作成します。
クラスが持つ属性の取得方法
- Attributeの値を取得するには、以下の手順で取得します。
// まずは型を取得 Type t = typeof(SettingClass); // 型が持つ属性を(この場合はIniFileAttribute属性)を取得 var att = (IniFileNameAttribute) Attribute.GetCustomAttribute(t, typeof(IniFileNameAttribute)); // 取得できなければ処理を抜ける if (att == null) return null; // 属性値、FileNameを取得 string iniFileName = att.FileName;
プロパティが持つ属性の取得方法
// まずは型を取得 Type t = typeof(SettingClass); // まずは型のプロパティを全て取得 var prof = t.GetProperties(); // 全て取得したプロパティをひとつづつチェックする foreach (var prop in prof) { // ----->>> 属性の有無をチェック var att2 = (IniFileAttribute) prop.GetCustomAttribute(typeof(IniFileAttribute)); // ----->>> 属性がなければ無視 if (att2 == null) continue; // ----->>> 属性からキーを得る string appName = att2.Application; string keyName = att2.Key; string defaultValue = att2.DefaultValue; // ----->>> 戻り値の準備 StringBuilder sb = new StringBuilder(256); // ----->>> INIから取得 uint res = GetPrivateProfileString(appName, keyName, defaultValue, sb, 256, iniFileName); }いかがですか?
クラスのプロパティに付与された属性の取得方法、意外と簡単ではないでしょうか。
後は、取得したプロパティの実体にデータを流し込むだけです。
- 以下は、
result
変数に対して、戻り値を設定しています。- サンプルは文字列と日付型のみを扱ってみています。
SettingClass result = new SettingClass(); // 文字列型のプロパティの場合 if (prop.PropertyType == typeof(string)) { // ----->>> 文字列の場合 prop.SetValue(result, sb.ToString()); } // 日付型のプロパティの場合 else if (prop.PropertyType == typeof(DateTime)) { // ----->>> 日付型の場合 if (DateTime.TryParse(sb.ToString(), out DateTime dmy)) prop.SetValue(result, dmy); }データを受け付けるクラスが肥大化しても、読み込みの部分をいちいち増やす必要はありません。
(扱うプロパティの型を増やせば当然コードを増やさなければなりませんが)
GetPrivateProfileString
コードを1行増やせば簡単のに、なぜこんなに手間を増やさなきゃならないの?とかいろいろご意見はあると思います。
今回はINIの話でしたが、属性を使えば相手がデータベースでも同様にできます。
(その場合はEntityFramework使えと怒られそうですが)
いろいろな使い方ができるので、属性を使ってみようかな、と思っていただけると幸いです。
完全な形は以下の通りとなります。
/// <summary> /// INIファイル操作系 /// </summary> public static class IniFileRWClass { #region INIファイル操作 /// <summary> /// INIファイルから文字列の取得 /// </summary> /// <param name="lpAppName">アプリケーション名</param> /// <param name="lpKeyName">キー</param> /// <param name="lpDefault">デフォルト値</param> /// <param name="lpReturnedString">戻り値</param> /// <param name="nSize">最大文字数</param> /// <param name="lpFileName">ファイル名</param> /// <returns>読み取りバイト数</returns> [DllImport("KERNEL32.DLL")] public static extern uint GetPrivateProfileString( string lpAppName, string lpKeyName, string lpDefault, StringBuilder lpReturnedString, uint nSize, string lpFileName); /// <summary> /// INIファイルへ文字列の書き込み /// </summary> /// <param name="lpAppName">アプリケーション名</param> /// <param name="lpKeyName">キー</param> /// <param name="lpString">書き込み文字数</param> /// <param name="lpFileName">ファイル名</param> /// <returns></returns> [DllImport("KERNEL32.DLL")] public static extern uint WritePrivateProfileString( string lpAppName, string lpKeyName, string lpString, string lpFileName); #endregion #region INI設定ファイル操作 /// <summary> /// 設定をファイルから取り込む /// </summary> /// <returns></returns> public static SettingClass ReadIniSetting() { //! ================================================================================ //! ----->>> 属性からINIファイル名を取得する(パスはEXEと同じフォルダにする) SettingClass result = new SettingClass(); Type t = typeof(SettingClass); var att = (IniFileNameAttribute) Attribute.GetCustomAttribute(t, typeof(IniFileNameAttribute)); if (att == null) return null; string iniFileName = att.FileName; string directry = Application.StartupPath; iniFileName = Path.Combine(directry, iniFileName); //! ================================================================================ //! ----->>> プロパティを検索し、属性値からアプリケーション名とキーを取得してINIファイルから値を得る var prof = t.GetProperties(); foreach (var prop in prof) { //? ----->>> 属性の有無をチェック var att2 = (IniFileAttribute) prop.GetCustomAttribute(typeof(IniFileAttribute)); if (att2 == null) continue; //? ----->>> 属性からキーを得る string appName = att2.Application; string keyName = att2.Key; string defaultValue = att2.DefaultValue; //? ----->>> 戻り値の準備 StringBuilder sb = new StringBuilder(256); //? ----->>> INIから取得 uint res = GetPrivateProfileString(appName, keyName, defaultValue, sb, 256, iniFileName); if (prop.PropertyType == typeof(string)) { //? ----->>> 文字列の場合 prop.SetValue(result, sb.ToString()); } else if (prop.PropertyType == typeof(DateTime)) { //? ----->>> 日付型の場合 if (DateTime.TryParse(sb.ToString(), out DateTime dmy)) { prop.SetValue(result, dmy); } } } //? ================================================================================ //? ----->>> 戻り値を返す return result; } /// <summary> /// 設定をファイルに書き込む /// </summary> /// <param name="data"></param> public static void WriteIniSetting(SettingClass data) { //! ================================================================================ //! ----->>> 属性からINIファイル名を取得する(パスはEXEと同じフォルダにする) Type t = typeof(SettingClass); var att = (IniFileNameAttribute)Attribute.GetCustomAttribute(t, typeof(IniFileNameAttribute)); if (att == null) return; string iniFileName = att.FileName; string directry = Application.StartupPath; iniFileName = Path.Combine(directry, iniFileName); //! ================================================================================ //! ----->>> プロパティを検索し、属性値からアプリケーション名とキーを取得してINIファイルへ値を書き込む var prof = t.GetProperties(); foreach (var prop in prof) { var att2 = (IniFileAttribute)prop.GetCustomAttribute(typeof(IniFileAttribute)); if (att2 == null) continue; //? ----->>> キーを取得 string appName = att2.Application; string keyName = att2.Key; //? ----->>> 値を文字列に変換する string Value = prop.PropertyType == typeof(DateTime) ? ((DateTime) prop.GetValue(data)).ToString("yyyy/MM/dd") : prop.GetValue(data).ToString(); //? ----->>> INIファイルに書き込む WritePrivateProfileString(appName, keyName, Value, iniFileName); } } #endregion }最後までお付き合いいただき、誠にありがとうございました。