2009年12月3日木曜日

.NET CSVファイルの読み込み

iniファイルのついでにCSVファイルを読み込むサンプルです。

CSVファイルの定義は以下の通りです。
1.レコードは、CRLFで区切られる。
2.フィールドの数は、同じである。
3.フィールドは、[区切文字]で区切られる。
4.フィールドに[区切文字]が含まれる場合、そのフィールドはダブルクォートで囲まれている。
 例)区切文字を「,(カンマ)」とした場合、○"field,111" ×fied,222
5.フィールドにダブルクォートが含まれる場合、そのフィールドはダブルクォートで囲まれており、
 フィールド内のダブルクォートを2つの連続するダブルクォートに置き換えている。
 例)○"field""111" ×"fied"222" ×field"333

ReadToDataTableメソッドは引数に指定したCSVファイルを
引数に指定したエンコードで読み込み、引数にしていした区切文字でフィールドに区切り、DataTabaleを返します。
引数に指定したisFirstTitleがtrueの場合、CSVファイルの先頭行はDataTableの列名になります。falseの場合はField1,Field2・・・となります。


2010/04/09
「CSVのフィールド数は同じである」としましたが、フィールド数がバラバラなCSVも存在しているのでそれらを読み込めるように修正しました。
あとバグもあったのでコッソリ修正しました。


Public Class CsvFile

    ''' <summary>
    ''' 1行づつ読み取りString配列で返す。
    ''' </summary>
    ''' <param name="path">CSVファイルパス</param>
    ''' <param name="enc">エンコード</param>
    ''' <remarks></remarks>
    Public Shared Function Read(ByVal path As String, ByVal enc As System.Text.Encoding) As String()
        Return System.IO.File.ReadAllLines(path, enc)
    End Function

    ''' <summary>
    ''' 指定したファイルから、指定した区切文字でフィールドに区切り、DataTableで返す。
    ''' </summary>
    ''' <param name="path">読み取るファイルのパス</param>
    ''' <param name="enc">エンコード</param>
    ''' <param name="cSeparator">区切文字</param>
    ''' <param name="isFirstTitle">1行目がタイトルかどうか</param>
    ''' <param name="sTblName">DataTable名</param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Shared Function ReadToDataTable(ByVal path As String, ByVal enc As System.Text.Encoding, ByVal cSeparator As Char, ByVal isFirstTitle As Boolean, ByVal sTblName As String) As DataTable

            'ファイルから読み込み
            Dim sLines As String() = Read(path, enc)

            'ファイルから読み込んだデータを分解し、リストに格納
            Dim lstRows As New List(Of String())
            For Each line As String In sLines
                '--行を分解しString配列にする。
                Dim fields As String() = LineToArray(line, cSeparator)
                lstRows.Add(fields)
            Next

            '読み込んだデータより最大列数を取得する
            Dim iMaxField As Integer
            For Each fields As String() In lstRows
                If iMaxField < fields.Length Then
                    iMaxField = fields.Length
                End If
            Next

            'テーブルに列を追加する。
            Dim tbl As New DataTable(sTblName)
            For idx As Integer = 0 To iMaxField - 1
                tbl.Columns.Add("Field" & (idx + 1), GetType(String))
            Next

            'テーブルにデータを追加する。
            For iRow As Integer = 0 To lstRows.Count - 1
                Dim fields As String() = lstRows(iRow)

                If isFirstTitle AndAlso iRow = 0 Then
                    For iCol As Integer = 0 To fields.Length - 1
                        tbl.Columns(iCol).ColumnName = fields(iCol)
                    Next
                Else
                    Dim newrow As DataRow = tbl.NewRow
                    For iCol As Integer = 0 To fields.Length - 1
                        newrow(iCol) = fields(iCol)
                    Next
                    tbl.Rows.Add(newrow)
                End If
            Next


            'Test出力
            '--------------------
            For idx As Integer = 0 To tbl.Columns.Count - 1
                If idx <> 0 Then Console.Write(",")
                Console.Write(tbl.Columns(idx).ColumnName)
            Next
            Console.WriteLine("")
            Console.WriteLine("-------------------------------")
            For Each row As DataRow In tbl.Rows
                For idx As Integer = 0 To row.ItemArray.Length - 1
                    If idx <> 0 Then Console.Write(",")
                    Console.Write(row(idx))
                Next
                Console.WriteLine("")
            Next
            '--------------------

            Return tbl
        End Function
        
        
        ''' <summary>
        ''' ファイルから読み取った1行分の文字列データを、引数に指定した区切り文字に従って分解し、String配列で返す
        ''' </summary>
        ''' <param name="line">ファイルから読み取った1行分の文字列データ</param>
        ''' <param name="cSeparator"></param>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Shared Function LineToArray(ByVal line As String, ByVal cSeparator As Char) As String()
            Dim lstField As New List(Of String)

            For iSt As Integer = 0 To line.Length - 1
                'フィールドの開始が"で始まっているかを判定
                Dim isWQuote As Boolean = False
                If line(iSt) = Chr(34) Then
                    isWQuote = True
                    iSt += 1
                End If
                'フィールドの終了位置を検索
                Dim sFind As String
                If isWQuote Then
                    sFind = Chr(34) + cSeparator
                Else
                    sFind = cSeparator
                End If
                Dim iEd As Integer = line.IndexOf(sFind, iSt)
                If iEd = -1 Then
                    iEd = line.Length
                    If isWQuote Then
                        If line(iEd - 1) = Chr(34) Then
                            iEd = line.Length - 1
                        End If
                    End If
                End If
                'フィールドを取得
                Dim sField As String
                sField = line.Substring(iSt, (iEd - iSt))
                'フィールドから2つの連続するダブルクォートを1つのダブルクォートに置換する
                sField = sField.Replace(Chr(34) + Chr(34), Chr(34))
                'フィールドを追加
                lstField.Add(sField)
                '開始インデックスをずらす
                If isWQuote Then
                    iSt = iEd + 1
                Else
                    iSt = iEd
                End If

            Next

            Return lstField.ToArray
        End Function

End Class
使用方法
Dim path As String = System.IO.Path.Combine(Application.StartupPath, "Test.csv")
Dim enc As System.Text.Encoding = System.Text.Encoding.GetEncoding("Shift_Jis")

'カンマ区切り
Dim tbl As DataTable = NvTool.File.CsvFile.ReadToDataTable(path, enc, ","c, True, "Test")

'Tab区切り
Dim tbl As DataTable = NvTool.File.CsvFile.ReadToDataTable(path, enc, Chr(9), True, "Test")

.NET iniファイルの読み込み

設定ファイルの読み書きは通常xmlファイルで行うのですが
ユーザーがファイルの編集を直接行う場合、iniファイルで作ることがあります。
設定画面を設けxmlファイルに保存する事がよいのではと思うのですが
そこは大人の事情で設定画面を設けずiniファイルでとなるわけですね。

そんな自分へのメモ。

iniファイルの記述形式は以下のように、keyとvalueが「=」で区切られており「//」以降はコメントとして無視します。
項目名(key)=値(value) //コメント

iniファイルを読み込みiniSettingクラスのフィールドに値を設定します。
iniファイルは直接編集が前提ですので、書き込みメソッドはありません。
またiniSettingクラスのプロパティもReadOnlyにしています。
アプリ全体で使用するのでiniSettingクラスはシングルトンにしています。
Public Class IniSetting

    '-----定数-----

    ''' <summary>Iniファイル名</summary>
    Private Shared ReadOnly IniFileName As String = "ini.txt"
    ''' <summary>Iniファイルパス</summary>
    Private Shared ReadOnly IniFilePath As String = System.IO.Path.Combine(Application.StartupPath, IniFileName)
    ''' <summary>エンコード</summary>
    Private Shared ReadOnly ENC As System.Text.Encoding = System.Text.Encoding.GetEncoding("Shift_Jis")

    '----staticフィールド----

    ''' <summary>自分自身のインスタンス</summary>
    Private Shared _Instance As IniSetting


    '----staticプロパティ----

    ''' <summary>自分自身のインスタンスを取得します</summary>
    Public Shared ReadOnly Property Instance() As IniSetting
        Get
            Return _Instance
        End Get
    End Property

    '----staticメソッド----

    ''' <summary>
    ''' 項目名=設定値  //コメント等
    ''' </summary>
    ''' <remarks></remarks>
    Public Shared Sub Load()
        'iniファイルよりに記載された項目名=設定値を読み取り、DictionaryのKeyとValueに設定します。
        Dim lstIni As New Dictionary(Of String, String)
        lstIni = LoadIni(IniFilePath, ENC)

        'IniSettingクラスのインスタンスを作成しフィールドに設定する
        Dim key As String
        _Instance = New IniSetting
        '--Item1
        key = "項目1"
        If Not lstIni.ContainsKey(key) Then
            Throw New FormatException(String.Format("{0}ファイルに{1}が設定されていません。", IniFileName, key))
        End If
        _Instance._Item1 = lstIni.Item(key).ToString
        '--Item2
        key = "項目2"
        If Not lstIni.ContainsKey(key) Then
            Throw New FormatException(String.Format("{0}ファイルに{1}が設定されていません。", IniFileName, key))
        End If
        If Integer.TryParse(lstIni.Item(key), _Instance._Item2) = False Then
            Throw New FormatException(String.Format("{0}ファイルの{1}が数値に変換できません。", IniFileName, key))
        End If
    End Sub

    ''' <summary>
    ''' iniファイルよりに記載された項目名=設定値を読み取り、DictionaryのKeyとValueに設定し返します。
    ''' </summary>
    ''' <param name="path"></param>
    ''' <param name="enc"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Private Shared Function LoadIni(ByVal path As String, ByVal enc As System.Text.Encoding) As Dictionary(Of String, String)
        Dim fileName As String = System.IO.Path.GetFileName(path)

        Dim lstIni As New Dictionary(Of String, String)
        If Not System.IO.File.Exists(path) Then
            Throw New System.IO.FileNotFoundException(String.Format("{0}ファイルを配置してください。", path))
        End If
        Dim sr As New System.IO.StreamReader(path, enc)
        Try
            While sr.Peek() > -1
                Dim s As String = sr.ReadLine
                If Not String.IsNullOrEmpty(s) Then
                    '--コメントを削除
                    s = System.Text.RegularExpressions.Regex.Replace(s, "//.*", "")
                    '--コメントまでにタブが入る可能性があるので削除
                    s = s.Replace(vbTab, String.Empty)
                    Dim ary As String() = s.Split("="c)
                    If ary.Length <> 2 Then
                        Throw New FormatException(String.Format("{0}ファイルの設定が不正です。" & vbCrLf & "項目名=設定値 の形式で記載されているか確認してください。", fileName))
                    Else
                        lstIni.Add(Trim(ary(0)), Trim(ary(1)))
                    End If
                End If
            End While
            sr.Close()
        Finally
            sr = Nothing
        End Try

        Return lstIni
    End Function

    '----コンストラクタ----

    ''' <summary>
    ''' コンストラクタ
    ''' </summary>
    ''' <remarks></remarks>
    Private Sub New()
        '隠匿
    End Sub

    '-----フィールド-----

    Private _Item1 As String
    Private _Item2 As Integer

    '-----プロパティ-----

    Public ReadOnly Property Item1() As String
        Get
            Return Me._Item1
        End Get
    End Property

    Public ReadOnly Property Item2() As Integer
        Get
            Return _Item2
        End Get
    End Property

End Class

使用方法
'アプリ起動時などでLoadしておく。
IniSetting.Load()

'IniSettingのInstance経由でプロパティにアクセスする。
Console.WriteLine(IniSetting.Instance.Item1)
Console.WriteLine(IniSetting.Instance.Item2)

2009年12月1日火曜日

.NET リソースファイルをプロジェクト間で共有する

リソースを管理するプロジェクトにリソースファイルを追加し画像などを追加します。
これを別のプロジェクトから利用する方法です。

リソースを管理するプロジェクト名を「ResouceProject」ととし、新しい項目の追加でリソースファイル「MyResouce」を追加します。
このリソースファイルに画像「image.jpeg」を追加しておきます。

方法その1

まず、別のプロジェクトからリソースファイルにアクセスできるように
リソースファイルのアクセス修飾子をデフォルトのFriendからPublicに変更します。



コードから利用するには以下のように記述します。
ResouceProject.My.Resouces.MyResouce.image

しかしこの方法では別のプロジェクトのプロパティウィンドウの「リソースの選択」ダイアログに表示されません。

方法その2

リソースファイルのアクセス修飾子をデフォルトのFriendのままにしておきます。

別のプロジェクトから「既存項目の追加」で「ResouceProject」プロジェクトの
「MyResouce.resx」「MyResouce.Designer.vb」を「リンクとして追加」で追加します。




コードから利用する場合は以下のように記述します。
「WindowsApplication1」はリソースを追加したプロジェクト名です。
Global.WindowsApplication1.MyResource.image

プロジェクトのプロパティウィンドウの「リソースの選択」ダイアログにも表示されます。