このページに含まれるプログラム、ファイル等については、
今回はプログラムではありませんが、最近知ってとても便利だったことを紹介します。みんな知ってるかもしれませんが。
それは名前に関数が使えること。
表を作っていて、日付や金額等の欄に名前を付けている場合、表が伸びるたびにすべての名前の範囲を変更しているあなた。ちょっとした工夫で自動的に範囲も伸びていきますが、それを怠ったときのメンテナンスが大変。
そんなときは表全体に名前(例えば小遣い帳)を付けましょう。(タイトル行を入れるかどうかはそのときの都合によりますが。) そして、日付の入っている列(例えばA)に名前を付けたい場合、[挿入][名前][定義]で、名前を「日付」と入力し、この名前「日付」の参照範囲を以下のように入力します。
=INDEX(小遣い帳,0,1)
これで、表の範囲の1列目(A)のデータ範囲が名前「日付」で定義されます。
2列目(B)なら、 例えば、名前を「項目」と入力し、次のように入力します。
=INDEX(小遣い帳,0,2)
表の範囲の定義は「小遣い帳」を変更するだけ。便利でしょ。「=」以下には自作VBAも使えますが、再計算同様の問題(参照先の数値の変化に対応しない)が生じることもあります。
さて、下で自作関数の入ったワークブックを作ったわけですが、他のワークブックで使おうと思ったら、
どうしたら良いでしょう?ひとつには、
=test.xls!test()
というようにして使うという方法があります。ただ、この場合は自分で自作関数の入ったファイルを
開ける必要があります。
もうひとつは「参照設定をしてやる」ということです。
やり方は簡単。あらかじめ、関数の入ったファイルを開けておき、VBE(Alt+F11で開くエディタのことです)
のプロジェクトウインドウで、自作関数の入ったプロジェクトを使用したいプロジェクトの中に
ドラッグするだけです。([ツール][参照設定]でも設定・解除が出来ます。)
注意点としては、二つのファイルのプロジェクト名を別々にしてください。
参照設定の場合はファイルを読み込んだ時点で、自動的に関数の入ったファイルが起動します。
(逆にこのファイルを閉じることが出来ません。)また、関数を呼ぶときも
=test()
で呼び出すことが出来ます。
ここで以下のプログラムがtestpというプロジェクトにあるとしましょう。
Private Function test()
test = 1
End Function
Public Function test2()
test2 = 2
End Function
Public Function test3()
test3 = test + test2
End Function
Private Sub test4()
MsgBox test
End Sub
Public Sub test5()
MsgBox test2
End Sub
これを参照設定したファイルではどのFunction/Subが使えるでしょうか?
答えはtest2、3、5です。
まず、Functionのみの話をしましょう。test()と他のものの違いはなんでしょう?
みれば分かりますね。test()だけは「Private」なのです。これは記述したファイル(プロジェクト)内で
のみ使えるということを示します。「public」あるいは書かない場合はどのファイルからでも参照できることになります。使う場合には
=testp.test2()
として使うことが出来ます。ファイル内に同じ名前の関数がない場合は「testp.」は不要です。
どうして「private」が必要なのでしょう。ひとつには同じ名前が多数存在すると混同をきたすために
共有が不要な関数はprivateにするということがあります。もうひとつは同じプロジェクト内の関数から
呼び出すためだけの関数である場合はprivateにして隠してしまうということです。これは不用意に
関数を呼び出されてエラーが起こってしまったり、重要な変数を書き換えられて誤った動作をしないように
するためです。
この「private」「public」は変数でも利用できます。Dimの代わりに
Public i As Integer
Private p As Integer
Private Const s As String = "test"
のように使うことが出来ます。
さて、Subの話ですが、ちょっとだけややこしいです。VBAからは確かにtest4は呼び出せません。
しかし、どのファイルがActiveであろうがExcelのマクロの実行では実行できてしまうのです。
これはそういうものだとするしかないようです。
私のVBAプログラミングの方法をここで書きたいと思います。私は解説本等を持っていませんので、勉強方法ともいえます。ポイントは
Selection.Height = 2 'あくまで例です。
ActiveSheet.Shapes("グラフ 1").ScaleWidth 0.69, msoFalse, msoScaleFromBottomRight ActiveSheet.Shapes("グラフ 1").ScaleHeight 0.73, msoFalse, msoScaleFromBottomRight
Selection.Parent.Parent.Height = 2
Dim nowShapeRange As ShapeRange If TypeName(Selection) = "ChartArea" Then Set nowShapeRange = Selection.Parent.Parent.ShapeRange Else Set nowShapeRange = Selection.ShapeRange End If nowShapeRange.Height = 30
8/10にもダイアログを使ったプログラムを紹介しています。Excelの「普通」の操作のために作られているので、使い勝手は悪いですが、上手に使えば下手なプログラムを組むより早く目標が達せられます。Border.xlsを試された方はいますか?色を取得するためにダイアログが出てくると思います。こんなもの私には作れません。
ここで、プログラムのミソを記したいと思います。
Option Explicit
Sub test()
Dim c As Variant
c = getColor
If Not IsNull(c) Then MsgBox c
End Sub
Function getColor() As Variant
If Application.Dialogs(xlDialogEditColor).Show(1) Then
getColor = ActiveWorkbook.Colors(1)
ActiveWorkbook.ResetColors
Else
getColor = Null
End If
End Function
If以下のApplication.Dialogs(xlDialogEditColor).Show(1)は「ColorIndexの1番の色を変更するよ」という意味です。キャンセルが押されるとFalseが帰ってきます。で、変更された1番の色を取得してから、リセットするという手順を取っています。キャンセルが押されたら、関数としてはNullを返しています。
いかがですか?
マクロを実行は基本的にやり直すことが出来ません。
あとで、選んだセルが間違っていることに気づいてもどうにもならないのです。
「基本的な命令ばっかり使っているのになぜ??」と思います。
ただし、ユーザーがやり直しを選択した場合に実行するマクロを指定することは可能です。
Dim temp As String
Sub test()
With ActiveCell
temp = .Address(External:=True)
.Value = .Value * 2
End With
Application.OnUndo "Undo test", "utest"
End Sub
Sub utest(Optional dummy As Integer = 0)
With Range(temp)
.Value = .Value / 2
End With
End Sub
OnUndoという命令をマクロの最後に記述することで、そのマクロを実行したあとにやり直しをした場合に、この場合ならutestが実行されます。そこに、「このマクロのUndoはこんな動作だな」と思うプログラムを組めば良いわけです。「なんで、マクロって言うの?」「何回もtestを実行したら記憶しているアドレスの関係でUndoの動作がおかしくなるんじゃないの?」と思われるかもしれません。理由は簡単です。
ExcelVBAでプログラミングする場合、どう考えても正しく動くはずなのにそうならない場合が往々にしてあります。これを「仕様」といいます(ホンマか?)。以下はExcel2000での「仕様」ですので、他のバージョンでは異なるかもしれません。
「2重線で普通の太さで5番の色の枠線」と「普通の太さで2重線で5番の色の枠線」とでは何が違うでしょう?常識で言って何も変わりませんね。変わるわけ無いですよね?
ところが、Excelの世界では通用しなかったりします。
Option Explicit
Sub test()
Dim b As Border
Dim w As XlBorderWeight '線の太さ
Dim l As XlLineStyle '線の種類
Dim c As XlColorIndex '線の色
w = xlMedium
l = xlDouble
c = 5
Range("A1").Clear
For Each b In Range("A1").Borders 'お勧め
With b
.Weight = w
.LineStyle = l
.ColorIndex = c
End With
Next b
Range("A3").Clear
For Each b In Range("A3").Borders
With b
.Weight = w
.ColorIndex = c
.LineStyle = l
End With
Next b
Range("C1").Clear
For Each b In Range("C1").Borders
With b
.LineStyle = l
.Weight = w
.ColorIndex = c
End With
Next b
Range("C3").Clear
For Each b In Range("C3").Borders
With b
.LineStyle = l
.ColorIndex = c
.Weight = w
End With
Next b
Range("E1").Clear
For Each b In Range("E1").Borders
With b
.ColorIndex = c
.LineStyle = l
.Weight = w
End With
Next b
Range("E3").Clear
For Each b In Range("E3").Borders
With b
.ColorIndex = c
.Weight = w
.LineStyle = l
End With
Next b
End Sub
これらで、6つのセルに同じ枠線が引かれる筈なのに違うことが分かってもらえると思います。バグとしか思えませんが。w,l,cの値をいろいろ変えると(初期設定のVBEなら、c以外は自動的に適当な値のリストが出ると思います。)、ちょっと遊べてそして結果に呆れてしまうと思います。
自分の思うとおりにVBAが動作しない場合、特に同じオブジェクトに対して、いろいろなプロパティの値を設定する場合には、設定順序を変えてやるとうまく行くこともあるでしょう。何とかなりませんかね。(前回と同じ引きだ…)
VBAでセルを参照するときにどう書きますか?普通はRange("A1")=1とか書きますか?これは書いてはいませんがRange("A1").Value=1と書いていることと同じです。(このValueを「既定のプロパティ」と呼びます。)
セルの参照方法にはValueの他にFormula、Text、Value2があります。これらはどう違うのでしょう?具体例を書いてみます。(Value2はValueとほとんど変わらないので省略します。)
つまり、Formulaは計算をしないそのままの文字列、Valueは計算後の値、Textはセルの見た目の文字列です。ちなみに上から分かるようにTextは文字列ですが、表示形式が標準で無い場合、Cstr(Range("A1"))[="1000"]と結果は異なることがあります。では、セルとセルの比較をするとき何を使えば良いでしょうか?A1: 100 B1: =A1*10 (B1には通貨(¥)の書式を設定してあるとします。) Msgbox Range("B1").Formula '"=A1*10" Msgbox Range("B1").Value '1000 Msgbox Range("B1").Text '"\1,000"
これはExcelは通貨を値を1000として、表示形式で通貨単位を表しているからです。A1: ロレックス A2: $1,000 B1: ロルックヌ B2: \1,000 Msgbox Range("A2")=Range("B2") 'True
VBAでプログラムを作っていると、「マクロ一覧に出したくない」プロシージャというのが出来てきます。 例えば、
TimerForm(ユーザーフォーム)
Option Explicit
Option Base 1
Private Full
Private Chara(3) As String
Private iCon As Integer
Private Sub Btn_Click()
Me.Hide
End Sub
Public Sub Start(EndValue, Optional Counter = 0#)
Dim Percent As Double, BarNum As Integer
Me.Btn.Caption = "中止"
Full = EndValue
Chara(1) = "."
Chara(2) = ".o"
Chara(3) = ".oO"
iCon = 0
Percent = Counter / Full
BarNum = Fix(Percent * 10)
Me.LblTxt.Font.Name = "MS ゴシック"
Me.LblTime.Font.Name = "system"
Me.LblTxt = Format(Percent, "000%") & " 終了"
Me.LblTime = String(BarNum, "O") & String(10 - BarNum, "-")
Me.Show vbModeless
End Sub
Public Function Update(Counter) As Boolean
Dim Percent As Double, BarNum As Integer
If Me.Visible Then
iCon = (iCon Mod 3) + 1
Percent = Counter / Full
BarNum = Fix(Percent * 10)
Me.LblTxt = Format(Percent, "000%") & " 終了 " & Chara(iCon)
Me.LblTime = String(BarNum, "O") & String(10 - BarNum, "-")
DoEvents
If Counter >= Full Then Me.Btn.Caption = "終了"
Update = False
Else
Update = True
End If
End Function
ユーザーフォーム内にBtnという名前のボタン、LblTxt、LblTimeという名のラベルがあれば、上のプログラムは動きます。
このユーザーフォームはBtnボタンを押すとフォームが非表示になり、Updateプロシージャを呼ぶとTrueを返すようになっています。実際の使用方法は次のとおりです。
Sub test()
Dim i As Integer, j As Long
Const Max As Integer = 500
TimerForm.Start Max
For i = 0 To Max
For j = 0 To 50
If TimerForm.Update(i) Then GoTo Finish
Next j
Next i
Finish:
End Sub
Startで最終の数値を指定してやり、ループごとにUpdateを呼んでやることによって、進行度のダイアログを変更していきます。処理の長くなるループを行うときに動いているかどうかわからなくなる不安を取り除き、途中で中止する手段も提供するために作りました。サンプルファイルをご覧ください。たとえ指定したカウンターの値が変化していなくても、アニメーションが続いていきます。このプログラムを入れることによって処理速度はもちろん遅くなりますので、程度を見て入れるようにしてください。
自作関数をワークシートで使っている場合で、セルの内容が変わったのに計算結果が変化しないことがありませんか?この原因のひとつに関数で直接セルを参照していない場合があげられます。
具体的に言うと、関数の引数でセルrを指定している場合、
Function MyIndirect(r As Range) MyIndirect = Range(r).Value End Function
Function MySum(r As Range) MySum = r + r.Offset(1) End Function
Function test(r As Range) 'どのセルが変化を受けても1ずつセルの内容が増えていきます。 Static i As Integer i = i + 1 test = r + i Application.Volatile End Function再計算の問題が解決する命令ですが、ただいろいろな関数につければ良いというのではなく、処理速度にかかわってくるので、必要な関数にのみ記述するようにしましょう。
イベント(ユーザーの操作など状況の変化に反応するマクロ)を記述している場合、「今だけ動くな」ということがあります。例えば、次の場合です。
Private Sub Worksheet_SelectionChange(ByVal Target As Range) Application.EnableEvents = False '操作の記述 Application.EnableEvents = True End Sub
ユーザー(ワークシート側)とVBAの間でデータをやり取りする際、その値はどこに保存しますか?
Const TestName As String = "Test" Sub NameRead() MsgBox Evaluate(Names(TestName).Value) '読み込み End Sub Sub NameWrite() Dim i As Integer i = 5 On Error Resume Next Err.Clear Names(TestName).Value = i '書き込み If Err.Number <> 0 Then '名前がない場合 Names.Add Name:=TestName, RefersToR1C1:="=" & i End If On Error GoTo 0 End Sub
ApplicationオブジェクトにはRunメソッドというものがあります。これはマクロを実行させるメソッドで、文字列で実行するマクロを指定します。通常のマクロ・関数の呼び出しと比べて以下のような利点があります。
Sub RunMyMacro(num As Integer) Application.Run Range("MyMacroList").Cells(num).Value End Sub
Function RunTest(v As Variant) On Error Resume Next RunTest = Application.Run("RunTest" & TypeName(v), v) If Err.Number <> 0 Then '例外処理 RunTest ="規定外:" & TypeName(v) End If On Error GoTo 0 End Function Function RunTestInteger(i As Integer) As String RunTestInteger = "Integer: " & i End Function Function RunTestDouble(d As Double) As String RunTestDouble = "Double: " & d End Function Function RunTestRange(r As Range) As String RunTestRange = "Range: " & r.Address(False, False) End Function
Sub VirtualCell() Dim VCell As String Dim Result As Variant VCell = InputBox("式を入れてください。") Result = Evaluate(VCell) If Not IsError(Result) Then MsgBox Result Else MsgBox "エラー: " & CInt(Result) End If End Sub
私は仕事柄、指数近似をよく使います。いつもはグラフに近似式を表示して傾きなどの値を読み取っているのですが、この値を別の式に代入してリアルタイムに変化させたい場合、いちいちグラフから読むわけにいきません。そこで、関数を使うのですが、例えばXの範囲がA1:C1,Yの範囲がA2:C2の以下のような場合、
A | B | C | |
1 | 0 | 1 | 2 |
2 | 100 | 50 | 25 |
=Exp(Index(LinEst(Ln(A2:C2), A1:C1, True, True), 1, 2))
Public Function 指数近似(mode As String, x As Range, Optional y As Range = Nothing) As Double If データセット(x, y) Then With Application.WorksheetFunction Select Case mode Case "傾き" 指数近似 = Evaluate("=Index(LinEst(Ln(" & y.Address & "), " & x.Address & ", True, True), 1, 1)") Case "切片" 指数近似 = Evaluate("=Exp(Index(LinEst(Ln(" & y.Address & "), " & x.Address & ", True, True), 1, 2))") Case "r2", "R2", "相関係数" 指数近似 = Evaluate("=Index(LinEst(Ln(" & y.Address & "), " & x.Address & ", True, True), 3, 1)") End Select End With Else 指数近似 = CVErr(11) End If End Function Private Function データセット(x As Range, y As Range) As Boolean If y Is Nothing Then If x.Rows.Count = 2 Then Set y = x.Rows(2) Set x = x.Rows(1) ElseIf x.Columns.Count = 2 Then Set y = x.Columns(2) Set x = x.Columns(1) Else データセット = False Exit Function End If End If データセット = True End Function
前回のおさらいです。物が一定の時間にある割合で減っていく様子は、次のような式で表されます。
% = Ae-kt
[%]は一定時間(t)過ぎた後の量で[A]は最初の量,[k]は減るスピードです。ある時間における残っている量を何点かグラフに書いていくと指数関数に近くなり、近似的に[A],[k]を求めることが出来ます。その求める式が次のとおりでした。
[A]: =Exp(Index(LinEst(Ln(A2:C2), A1:C1, True, True), 1, 2))
[k]: =Index(LinEst(Ln(A2:C2), A1:C1, True, True), 1, 1)
前回のプログラムはこれを求めるのに式を羅列していましたが、やはり美しくありません。メンテナンスも大変です。また、欲深く、当てはめた近似式からある時間(t)における割合(%)の予想や逆にある一定の割合に減るのにかかる時間が求まらないか?また、ある一点だけ代入して(0時間の量を100として)、式を求めることも出来ないか?といろいろ考えました。
そこで、次のようなプログラムになります。
Public Function DegFunc(mode As String, x As Range, Optional y As Range = Nothing, Optional v As Variant) As Double Dim k As Double, A As Double, R2 As Double Dim RA As Variant Const DefY As Double = 100 If DegFunc_Separation(x, y) Then If x.Cells.Count = 1 And y.Cells.Count = 1 Then RA = Evaluate("LinEst(Ln({" & DefY & "," & y & "}), {0," & x & "}, True, True)") Else RA = Evaluate("LinEst(Ln(" & y.Address & "), " & x.Address & ", True, True)") End If k = -RA(LBound(RA), LBound(RA)) A = Exp(RA(LBound(RA), LBound(RA) + 1)) R2 = RA(LBound(RA) + 2, LBound(RA)) With Application.WorksheetFunction Select Case mode Case "k": DegFunc = k '分解速度 Case "A": DegFunc = A 'Y切片 Case "r2", "R2": DegFunc = R2 '相関係数(決定係数) Case "DT50", "T1/2": DegFunc = Log(2) / k '半減期(50%になるまでの期間) Case "DT90": DegFunc = Log(10) / k '90%になるまでの期間 Case "t": DegFunc = Log(A / CDbl(v)) / k '一定の割合になるまでの期間 Case "%": DegFunc = A * Exp(-k * CDbl(v)) '一定期間後の割合 End Select End With Else DegFunc = CVErr(11) End If End Function Private Function DegFunc_Separation(x As Range, Optional y As Range) As Boolean If y Is Nothing Then If x.Rows.Count = 2 Then Set y = x.Rows(2) Set x = x.Rows(1) ElseIf x.Columns.Count = 2 Then Set y = x.Columns(2) Set x = x.Columns(1) Else DegFunc_Separation = False Exit Function End If End If DegFunc_Separation = True End Function
さて、前回まで自作関数を作ってきました。この関数をほかのワークブックで使うにはどうしたらいいでしょう?
一つには標準モジュールをそのままコピーすることが考えられます。この方法は最も手軽ですが、使いたいワークブックに全部コピーするのはめんどくさいですよね。他には参照設定をするという方法があります。こちらは、以前紹介しています。ただ、こちらも使いたいワークブック全てで設定する必要があります。
今回は「アドイン」にしてしまうという方法を紹介します。実は前から、SuperSubのファイルに書いてあるのですが。
そのまま引用してみます。
常に使う予定であれば、「名前を付けて保存」の「ファイルの種類」で 「Microsoft Excel アドイン」(一番下です)を選択して保存し、 Excelを再起動した後、「ツール」→「アドイン」でSupersubにチェックを入れてください。 次回から、Excelを立ち上げるたびに自動的に起動されます。 (マクロの警告パネルも出なくなります。)アドインにする利点は「関数が常に利用できる」「マクロチェックをしなくなる」という点が上げられます。欠点としては「ワークシートを見ることが出来なくなる。」「配布には向かない(各コンピュータでアドインを設定する必要がある)」という点が上げられます。
数式でセル参照をしているかどうかを調べる条件式を考えてみました。
If ActiveCell.Formula <> ActiveCell.FormulaR1C1 Then MsgBox "数式内にセル参照があります。" End If