CSVファイル入出力補助関数
CSVファイルを読み書きする補助関数として、一行分のCSV文字列をカンマで配列に分割するSplitCSV関数と、配列をCSV文字列に結合するJoinCSV関数を作りました。いちおう括り文字と前ゼロを考慮しています。
Option Explicit Const vbDoubleQuote$ = """" Const vbComma$ = "," Private Sub Command1_Click() Dim i As Long Dim arr() As String Dim f As Integer Dim line As String f = FreeFile Open "C:\Documents and Settings\love4beer\デスクトップ\test.csv" For Input As #f Do Until EOF(f) Line Input #f, line Debug.Print "line:" & line arr = SplitCSV(line) For i = LBound(arr) To UBound(arr) Debug.Print i & ":" & arr(i) Next Debug.Print "join:" & JoinCSV(arr) Loop Close #f End Sub Function SplitCSV(line As String) As String() Dim i As Long Dim quoted As Boolean 'ダブルクォートの内側のときTrue/それ以外のときFalse Dim arr() As String quoted = False '区切文字のカンマをバックスペース(CSVファイルに入力できない値)に置換する For i = 1 To Len(line) Select Case Mid$(line, i, 1) Case vbDoubleQuote quoted = Not quoted Case vbComma If Not quoted Then Mid$(line, i, 1) = vbBack End If End Select Next 'バックスペースでSplit arr = Split(line, vbBack) For i = LBound(arr) To UBound(arr) '括り文字を外す(一旦タブに置換して、後でまとめて削除する) If arr(i) Like vbDoubleQuote & "*" & vbDoubleQuote Then Mid$(arr(i), 1, 1) = vbTab Mid$(arr(i), Len(arr(i)), 1) = vbTab End If 'Trim、エスケープを元に戻す、タブを削除する arr(i) = Replace$(Replace$( _ Trim$(arr(i)), _ vbDoubleQuote & vbDoubleQuote, vbDoubleQuote), _ vbTab, vbNullString) Next Let SplitCSV = arr End Function Function JoinCSV(arr() As String) As String Dim i As Long For i = LBound(arr) To UBound(arr) 'ダブルクォートで括る(データ中のダブルクォートをエスケープする) '前ゼロコードにはタブを付加する arr(i) = vbDoubleQuote & _ IIf(IsNumeric(arr(i)) And arr(i) Like "0#*", vbTab, vbNullString) & _ Replace$(arr(i), vbDoubleQuote, vbDoubleQuote & vbDoubleQuote) & _ vbDoubleQuote Next Let JoinCSV = Join(arr, vbComma) End Function
■追記■
SplitCSV関数を、ExcelのCSV解析に準拠させてみました。最初のForループで、括りとエスケープのダブルクォートをvbTabに、区切りのカンマをvbBackに置換し、その後、Replaceでタブを除去して、Splitで配列に分割しています。
Enum FindState FindOpen FindClose FindComma End Enum Function SplitCSV(ByVal line As String) As String() Dim i As Long Dim fs As FindState fs = FindState.FindOpen For i = 1 To Len(line) Select Case fs Case FindState.FindOpen '項目の先頭で括りの開始を探す Select Case Mid$(line, i, 1) Case vbDoubleQuote Mid$(line, i, 1) = vbTab fs = FindState.FindClose Case vbComma '値なしの項目 Mid$(line, i, 1) = vbBack Case Else fs = FindState.FindComma End Select Case FindState.FindClose '括りの内側である場合に限り括りの終了を探す If Mid$(line, i, 1) = vbDoubleQuote Then Mid$(line, i, 1) = vbTab '次の文字をチェックする If Mid$(line, i + 1, 1) = vbDoubleQuote Then 'エスケープされたダブルクォートはスキップする i = i + 1 Else '単独のダブルクォートなので括りの終了 fs = FindState.FindComma End If End If Case FindState.FindComma '括りの内側でない場合に限りカンマを探す If Mid$(line, i, 1) = vbComma Then Mid$(line, i, 1) = vbBack fs = FindState.FindOpen End If End Select Next Let SplitCSV = Split(Replace$(line, vbTab, vbNullString), vbBack) End Function
ListBoxの項目を「上へ」「下へ」
UserFormにListBoxとSpinButtonをポトリペタして以下のコードを貼り付けます。
Option Explicit Private Sub UserForm_Initialize() ListBox1.MultiSelect = fmMultiSelectExtended Dim i As Long For i = 1 To 10 ListBox1.AddItem "hoge:" & i Next End Sub Private Sub SpinButton1_SpinUp() Call UpSelectedItems(ListBox1) End Sub Private Sub SpinButton1_SpinDown() Call DownSelectedItems(ListBox1) End Sub Private Sub UpSelectedItems(l As MSForms.ListBox) Dim i As Long For i = 0 To l.ListCount - 1 Call Swap(l, i, i - 1) Next End Sub Private Sub DownSelectedItems(l As MSForms.ListBox) Dim i As Long For i = l.ListCount - 1 To 0 Step -1 Call Swap(l, i, i + 1) Next End Sub Private Sub Swap(l As MSForms.ListBox, a As Long, b As Long) If a < 0 Or a > l.ListCount - 1 Then Exit Sub If b < 0 Or b > l.ListCount - 1 Then Exit Sub If a = b Then Exit Sub 'Index:a が選択されていて Index:b が選択されていないとき、aとbを入れ替える If l.Selected(a) And Not l.Selected(b) Then Dim v As Variant v = l.List(a) l.RemoveItem a l.AddItem v, b l.Selected(a) = False l.Selected(b) = True End If End Sub
VB6コンパイルスクリプト
まとめてコンパイルするときに使うスクリプト。他の言語でもコマンドラインをアレンジして使える。それとディレクトリサーチのテンプレ。
'----------------------------------------------------------------------- ' VB6コンパイルスクリプト '----------------------------------------------------------------------- ' カレント配下のvbp(VBプロジェクトファイル)を検索して、 ' binフォルダへexeを出力します ' ├vb6.vbs ' ├projectA ┐ ' ├projectB │入力 ' ・・・ │ ' ├projectZ ┘ ' └bin ┐ ' ├A.exe │出力 ' ├B.exe │ ' ・・・ │ ' └Z.exe ┘ '----------------------------------------------------------------------- Option Explicit const vb6exe = "C:\Program Files\Microsoft Visual Studio\VB98\vb6.exe" const vbHide = 0 dim vbp dim outdir dim wsh dim fso dim root dim msg init dive root fini msgbox msg & "コンパイルが完了しました。" WScript.Quit sub init() set wsh = CreateObject("Wscript.Shell") set fso = CreateObject("Scripting.FileSystemObject") set root = fso.GetFolder(fso.GetParentFolderName(Wscript.ScriptFullName)) outdir = fso.BuildPath(root.Path, "bin") if not fso.FolderExists(outdir) then fso.CreateFolder(outdir) end if end sub sub fini() set wsh = Nothing set fso = Nothing set root = Nothing end sub sub dive(p) dim f for each f in p.Files compile f next dim c for each c in p.SubFolders dive c next end sub sub compile(f) 'vbpファイル以外はスキップ if strcomp("vbp", fso.GetExtensionName(f.Name), vbTextCompare) <> 0 then exit sub end if msg = msg & f.Name & vbCrLf vbp = f.Path wsh.Run cmd("""%vb6exe%"" /make ""%vbp%"" -outdir ""%outdir%"""), vbHide, True end sub function cmd(s) 'パラメータを展開する cmd = s with CreateObject("VBScript.RegExp") .Pattern = "%([^%]+)%" .Global = True do while .Test(cmd) dim par for each par in .Execute(cmd) Execute "cmd = Replace(cmd, par, " + par.subMatches(0) + ")" next loop end with end function
fpSpreadでVirtualModeを簡単に使う拡張クラス
スプレッドに表示するデータをデータベースから取得しているとします。セルには複雑めの書式を設定していて、3000件くらい読み込むと、うーーーーーんぱ、くらい待たされます。というときに、うぱっと開くためのfpSpread拡張クラスです。
うぱっと開くにはVirtualModeを使います。VirtualModeは画面に表示しそうな範囲だけを処理対象にすることで高速化しています。デフォルトではQueryDataイベントがこれにあたりますが、1レコード複数行のレイアウトを使っていると、データソースとスプレッドの表示位置を対応付ける計算が微妙にめんどくさいので、カレント1レコードごとにQueryRowイベントを発火させるようにしてみました。
fpSpreadEx.cls
Option Explicit Private WithEvents This As fpSpread Private OraDynaset As OraDynaset Private OnFirst As Boolean Private SpreadFormat As SpreadFormat Public Event QueryRow() '1レコード分のデータ要求イベント Public Event OnFirstQuery() '初回要求時のイベント ' 初期処理 Public Sub Init(Spread As fpSpread, OraDynaset As OraDynaset) Set This = Spread Set OraDynaset = OraDynaset Set SpreadFormat = GetSpreadFormat(This) 'スプレッドから1レコード分のフォーマットを読み込む処理 OnFirst = True This.VirtualMaxRows = OraDynaset.RecordCount * RecordRows This.VirtualRows = 100 * RecordRows This.VirtualOverlap = 0 This.VirtualMode = True End Sub Public Property Get RecordRows() As Long RecordRows = This.ColHeaderRows End Property Public Property Get RecordCols() As Long RecordCols = This.MaxCols End Property Private Function RowToRowPosition(Row As Long) As Long RowToRowPosition = (Row - 1) \ RecordRows + 1 End Function Private Function CurrentTopRow() As Long CurrentTopRow = (OraDynaset.RowPosition - 1) * RecordRows + 1 End Function Private Function CurrentBottomRow() As Long CurrentBottomRow = OraDynaset.RowPosition * RecordRows End Function 'SpreadデフォルトのQueryDataイベント Private Sub This_QueryData(ByVal Row As Long, ByVal RowsNeeded As Long, RowsLoaded As Long, ByVal Direction As Integer, AtTop As Boolean, AtBottom As Boolean) 'Row 'スプレッド先頭Row 'Row + RowsNeeded - 1 'スプレッド末尾Row Dim lngTopRowPosition As Long 'Dynaset先頭RowPosition Dim lngEndRowPosition As Long 'Dynaset末尾RowPosition lngTopRowPosition = RowToRowPosition(Row) lngEndRowPosition = RowToRowPosition(Row + RowsNeeded - 1) With This Call OraDynaset.MoveTo(lngTopRowPosition) Do While Not OraDynaset.EOF If OraDynaset.RowPosition > lngEndRowPosition Then Exit Do End If 'スプレッド書式設定 If OraDynaset.RowPosition = lngTopRowPosition Or _ This.ActiveRow = CurrentTopRow - RecordRows Then Call SetFormat Else Call CopyFormat End If If This.ActiveRow = CurrentTopRow Then Call SetSelectedFormat End If 'RowPositionのデータ要求イベントを発火 RaiseEvent QueryRow OraDynaset.MoveNext Loop If OnFirst Then OnFirst = False '初回要求時のイベントを発火 RaiseEvent OnFirstQuery End If End With RowsLoaded = RowsNeeded AtTop = (Row = 1) AtBottom = OraDynaset.EOF End Sub Private Sub SetFormat() 'Initで読んでおいたSpreadFormat(スプレッドの1レコード分のセルフォーマット)を、 'スプレッドに適用する処理を記載する End Sub Private Sub SetSelectedFormat() '選択行の文字色、背景色の切り替えなどはここで End Sub Private Sub CopyFormat() '1レコード前のセルフォーマットをまるっとコピーする Call This.CopyRowRange(CurrentTopRow - RecordRows, _ CurrentBottomRow - RecordRows, _ CurrentTopRow) End Sub
使い方
拡張クラスのInitでスプレッドとデータセットを紐付けます。
Begin FPSpreadADO.fpSpread Spread Public OraDynaset As OraDynaset Private WithEvents SpreadEx As fpSpreadEx '読み込みボタン押下時の処理 Set OraDynaset = OraDataBase.CreateDynaset(SQL, GC_ORADYN_READONLY) Call SpreadEx.Init(Spread, OraDynaset)
要求1レコードごとにQueryRowイベントが発火するので、ここで、データセットのCurrentをスプレッドのCurrentTopRowへ貼り付けます。
Private Sub SpreadEx_QueryRow() Dim arr() As String ReDim arr(1 To SpreadEx.RecordRows, 0 To SpreadEx.RecordCols) With OraDynaset '行番号 arr(1, 0) = .RowPosition 'データ 1段目 arr(1, 1) = .Fields("COLUMN1").Value arr(1, 2) = .Fields("COLUMN2").Value arr(1, 3) = .Fields("COLUMN3").Value 'データ 2段目 arr(2, 1) = .Fields("COLUMN4").Value arr(2, 2) = .Fields("COLUMN5").Value arr(2, 3) = .Fields("COLUMN6").Value End With Call Spread.SetArray(0, SpreadEx.CurrentTopRow, arr) End Sub
ちなみに、ユーザーが操作した直後の一回だけの処理(例えばSetFocusとかEnabledを切り替えとか)はOnFirstQueryイベントに記述します。
Private Sub SpreadEx_OnFirstQuery() 'スプレッドでアクティブ行を切り替えたときの処理など End Sub
rpxテキストを読み込んで動的にレイアウト
ActiveReportsを使ったVB6プロジェクトで、ユーザーごとに、ちょこっとだけレイアウトを変えたいときにお手軽に対応する方法
ActiveReportは、rpxというXML形式のレイアウト定義を読み書きできるので、ちょこっと変えたレイアウト(dsrとdsx)からrpxをそれぞれ作り、実行時にはこれを読み込んでレイアウトを変更するというやり方です。rpxファイルを配布したくなかったので、データベースから読み込むことにしました。rpxファイルはUTF-8、データベースから読み込んだ文字列(VB6内)はUTF-16LEなので、UTF16ToUTF8関数も作りました。
ユーザーごとのレイアウト定義を用意します。
UserReport.dsr UserReport.dsx UserReport.rpx
前記事の rpxファイルをDBへインポートする で UserReport.rpx をDBにインポートしておきます。アプリ実行時には、これを読み込んでLoadLayoutします。
呼び出し
Dim obj As UserReport Set obj = New UserReport Call LoadLayout(obj, "UserReport.rpx")
共通関数
Declare Function WideCharToMultiByte Lib "kernel32" _ (ByVal CodePage As Long, _ ByVal dwFlags As Long, _ ByVal lpWideCharStr As Long, _ ByVal cchWideChar As Long, _ ByRef lpMultiByteStr As Any, _ ByVal cchMultiByte As Long, _ ByVal lpDefaultChar As String, _ ByVal lpUsedDefaultChar As Long) As Long Public Sub LoadLayout(ByRef obj As ActiveReport, ReportID As String) Dim SQL As String Dim OraDynaset As OraDynaset Dim arr() As String Dim rpx() As Byte Dim i As Long 'バインドパラメータ設定 With OraDataBase.Parameters For i = .Count - 1 To 0 Step - 1 .Remove i Next .Add "ReportID", "", ORAPARM_INPUT, ORATYPE_VARCHAR2 .Item("ReportID").Value = UCase(ReportID) End With SQL = "SELECT LINE FROM ActiveReports WHERE ReportID = :ReportID ORDER BY LINE_NO" Set OraDynaset = OraDataBase.CreateDynaset(SQL, ORADYN_READONLY) With OraDynaset If Not .EOF Then 'データベースからrpxテキストをarrに読み込み ReDim arr(1 To .RecordCount) Do While Not .EOF arr(.RowPosition) = .Fields("LINE").Value & vbCrLf '行末に改行を付加 .MoveNext Loop rpx = UTF16ToUTF8(Join(arr, "")) Call obj.LoadLayout(rpx) End If End With End Sub Public Function UTF16ToUTF8(UTF16 As String) As Byte() Const CP_UTF8 = 65001 UTF16ToUTF8 = UnicodeToMultiByte(UTF16, CP_UTF8) End Function Public Function UnicodeToMultiByte(UTF16 As String, CodePage As Long) As Byte() Dim pInput As Long Dim pOutput As Long Dim size As Long Dim buf() As Byte 'バッファサイズを取得するために、空変換してサイズだけ取得する pInput = StrPtr(UTF16) size = WideCharToMultiByte(CodePage, 0, pInput, -1, 0&, 0&, ByVal 0&, ByVal 0&) 'バッファを確保してCodePageに変換する ReDim buf(size - 1) pOutput = VarPtr(buf(0)) size = WideCharToMultiByte(CodePage, 0, pInput, -1, pOutput, size, ByVal 0&, ByVal 0&) '末尾のnullを除去する ReDim Preserve buf(size - 2) UnicodeToMultiByte = buf End Function
rpxファイルをDBへインポートする
ActiveReportsのrpxファイルを、データベースにインポートするVBScript
UTF-8のリソースファイルをDBにインポートするときのテンプレ
CREATE TABLE ACTIVEREPORTS ( REPORTID VARCHAR2(32) NOT NULL, LINE_NO NUMBER(5,0) NOT NULL, LINE VARCHAR2(2000) CONSTRAINT ACTIVEREPORTS_PK PRIMARY KEY (REPORTID, LINE_NO) USING INDEX )
Option Explicit '動作環境変数 dim oraconst dim database dim username dim password oraconst = "C:\Oracle\Ora92\oo4o\ORACONST.TXT" ' 設定しなければ %ORACLE_HOME% を探しに行きます database = "database" username = "username" password = "password" 'Globalオブジェクト dim wsh dim fso dim ado dim OraSession dim OraDatabase 'ADODB定数 const adBinary = 1 const adText = 2 const adUTF8 = "UTF-8" const adReadAll = -1 const adReadLine = -2 dim arg dim msg call init() for each arg in WScript.Arguments msg = msg & fso.GetFilename(arg) & vbCrLf call main(arg) next call fini() WScript.Echo msg & vbCrLf & "を ActiveReports へインポートしました" WScript.Quit sub init() set wsh = CreateObject("WScript.Shell") set fso = CreateObject("Scripting.FileSystemObject") if WScript.Arguments.Count = 0 then call err.Raise(vbObjectError + 1,, "*.rpx(ActiveReportsレポート定義ファイル) をドロップして下さい") end if for each arg in WScript.Arguments if StrComp("rpx", fso.GetExtensionName(arg), vbTextCompare) <> 0 then call err.Raise(vbObjectError + 2,, fso.GetFilename(arg) & " は *.rpx(ActiveReportsレポート定義ファイル) ではありません") end if next 'ORACONST.TXT定義展開 if len(oraconst) = 0 then if StrComp("%ORACLE_HOME%", wsh.ExpandEnvironmentStrings("%ORACLE_HOME%"), vbTextCompare) = 0 then call err.Raise(vbObjectError + 3,, "環境変数 ORACLE_HOME を取得できません") end if oraconst = fso.BuildPath(wsh.ExpandEnvironmentStrings("%ORACLE_HOME%"), "oo4o\ORACONST.TXT") end if if fso.FileExists(oraconst) then ExecuteGlobal Replace(fso.OpenTextFile(oraconst).ReadAll, "Global ","") else call err.Raise(vbObjectError + 4,, oraconst & " ファイルが見つかりません") end if '入力ファイル設定 Set ado = CreateObject("ADODB.Stream") ado.Type = adText ado.Charset = adUTF8 'ORACLE接続 Set OraSession = CreateObject("OracleInProcServer.XOraSession") Set OraDatabase = OraSession.OpenDatabase(database, username & "/" & password, ORADB_DEFAULT) OraDatabase.Parameters.Add "REPORTID", 0, ORAPARM_INPUT OraDatabase.Parameters.Add "LINE_NO" , 0, ORAPARM_INPUT OraDatabase.Parameters.Add "LINE" , 0, ORAPARM_INPUT end sub sub main(filename) dim reportid dim line_no dim line reportid = ucase(fso.GetFilename(filename)) line_no = 0 ado.Open ado.LoadFromFile filename OraSession.BeginTrans OraDatabase.Parameters("REPORTID").AutoBindEnable OraDatabase.Parameters("LINE_NO").AutoBindDisable OraDatabase.Parameters("LINE").AutoBindDisable OraDatabase.Parameters("REPORTID").Value = reportid OraDatabase.ExecuteSQL "DELETE FROM ActiveReports WHERE REPORTID = :REPORTID" OraDatabase.Parameters("REPORTID").AutoBindEnable OraDatabase.Parameters("LINE_NO").AutoBindEnable OraDatabase.Parameters("LINE").AutoBindEnable do while not ado.EOS line_no = line_no + 1 line = ado.ReadText(adReadLine) OraDatabase.Parameters("REPORTID").Value = reportid OraDatabase.Parameters("LINE_NO").Value = line_no OraDatabase.Parameters("LINE").Value = line OraDatabase.ExecuteSQL "INSERT INTO ActiveReports VALUES (:REPORTID, :LINE_NO, :LINE)" loop OraSession.CommitTrans ado.Close end sub sub fini() set wsh = Nothing set fso = Nothing set ado = Nothing set OraDatabase = Nothing set OraSession = Nothing end sub
ReplaceTable
var tablename varchar2(32) var yyyymmdd varchar2(8) var scriptdir varchar2(512) execute :tablename := '&1'; execute :yyyymmdd := '&2'; execute :scriptdir := '&3'; select statement from( select 0 as no, '-- ■■■ テーブル名( ' || :TABLENAME || ' )' as statement from dual union all select 0.1 as no, 'SELECT TO_CHAR(SYSDATE,''YYYY/MM/DD HH24:MI:SS'') FROM DUAL' from dual union all select 0.9 as no, '/' from dual union all select 1 as no, 'CREATE TABLE ' || :TABLENAME||'_'||:YYYYMMDD || ' AS SELECT * FROM ' || :TABLENAME from dual union all select 1.9 as no, '/' from dual union all select 2 as no, 'DROP TABLE ' || :TABLENAME || ' CASCADE CONSTRAINTS' from dual union all select 2.9 as no, '/' from dual union all select 3 as no, '@"'||:SCRIPTDIR||'\'||:TABLENAME||'.SQL"' from dual union all select 3.9 as no, '/' from dual union all select 4 as no, 'INSERT INTO ' || :TABLENAME || '(' from dual union all select 4 + column_id / 1000 as no, decode(column_id,1,' ',',') || column_name from user_tab_columns where table_name = :TABLENAME union all select 4.2 as no, ')' from dual union all select 4.3 as no, 'SELECT * FROM ' || :TABLENAME||'_'||:YYYYMMDD from dual union all select 4.9 as no, '/' from dual union all select 5 as no, 'COMMIT' from dual union all select 5.9 as no, '/' from dual union all select 90 as no, 'SELECT ''件数チェック'',OLD.COUNT,NEW.COUNT' from dual union all select 90.1 as no, ',''■■■''||DECODE(OLD.COUNT, NEW.COUNT, ''OK'', ''※NG※'') FROM' from dual union all select 90.2 as no, ' (SELECT COUNT(*) COUNT FROM ' || :TABLENAME||'_'||:YYYYMMDD||') OLD' from dual union all select 90.3 as no, ',(SELECT COUNT(*) COUNT FROM ' || :TABLENAME || ' ) NEW' from dual union all select 90.9 as no, '/' from dual union all select 99 as no, '-- ワークテーブル削除' from dual union all select 99.1 as no, '-- DROP TABLE ' || :TABLENAME||'_'||:YYYYMMDD from dual union all select 99.9 as no, '' from dual union all select 99.9 as no, '' from dual order by no) /