ラベル AdventCalendar の投稿を表示しています。 すべての投稿を表示
ラベル AdventCalendar の投稿を表示しています。 すべての投稿を表示

2012年12月24日月曜日

[ExcelVBA] 二次元配列を簡単に操作するClass


アドベントカレンダー 24日目

データを蓄積してそれを最後にセルに出力するなんてことはあると思うのですが
データ量が増えるとエクセルに出力する部分がえらく時間がかかるようになります
高速で出力するには、配列に格納して出力するのが最速となります。
ただ…
出力する時に行と列をいれかえる必要があったりCollectionならAddするだけで
要素が追加できるとこをいちいち、要素数が存在しない時と存在する時で処理を
分岐して毎回Redim Preserveしなきゃいけないなんてのはめんどくさい以外の
何者でもありません。

そういうのは、裏側でこっそり処理して表側には見えないようにしてあげた方が便利よいですよね。
そんなわけで、セルに出力することに特化させた二次元配列のクラスを作ってみました。
Option Explicit

Dim Data()    As String
Dim x         As Long
Dim y         As Long
Dim DelColumn As Integer

'Setter/Getter
Public Property Let DeleteColumn(ByVal num As Integer)
    DelColumn = num
End Property

Public Property Get DeleteColumn() As Integer
    DeleteColumn = DelColumn
End Property

Private Sub Class_Initialize()

    '初期化
    x = -1
    y = -1

End Sub

Function Count()

    '要素数を返す
    Count = UBound(Data, 2) + 1

End Function

Function Item(ByVal a As Long, ByVal b As Long)
    
    '要素の値を返す
    Item = Data(a, b)
    
End Function

Function Last()

    '最終要素を返す
    Last = UBound(Data, 2)

End Function

Private Function TransposeData()
    Dim s        As Long
    Dim t        As Long
    Dim TPData() As String
    ReDim TPData(y, x)
    
    For s = 0 To y
        For t = 0 To x
            TPData(s, t) = Data(t, s)
        Next
    Next
    
    TransposeData = TPData
End Function

Function Value() As String()

    '2次元配列自体を返す
    Value = Data()

End Function

Sub Add(ByRef list As Variant)
    Dim i As Long
    
    '1次元の要素数が確定していない時、要素数の調査をおこなう
    If x = -1 Then
        
        If TypeName(list) = "Variant()" Then
            
            x = UBound(list)
        
        Else
            
            x = 0
        
        End If
    
    End If
    
    
    '要素数の再定義
    y = y + 1
    ReDim Preserve Data(x, y)
    
    If TypeName(list) = "Variant()" Then
        
        For i = 0 To x
        
            '要素数が足りないときは、空文字を指定
            On Error Resume Next
            Data(i, y) = list(i)
            On Error GoTo 0
        
        Next
        
    Else
    
        Data(x, y) = list
    
    End If

End Sub

Sub Clear()
    Erase Data
    DelColumn = -1
    x = -1
    y = -1
End Sub

Sub Output(ByVal Sht As Worksheet, ByVal Cell As String)
    Dim BaseRow As Long
    Dim BaseCol As Long
    
    BaseRow = Sht.Range(Cell).Row
    BaseCol = Sht.Range(Cell).Column
    
    'DelColumnが1以上でセットされている時該当列をクリアする
    If 0 < DelColumn Then
        Sht.Range(Sht.Cells(BaseRow, BaseCol), Sht.Cells(65536, BaseCol + DelColumn)).Clear
    End If
    
    Sht.Range(Sht.Cells(BaseRow, BaseCol), Sht.Cells(BaseRow + y, BaseCol + x)) = TransposeData()
    
End Sub
好きな名前でもつけてくださいな。 使い方ですがサンプル内にコメントで追加しました。 ■値の追加、値の取り出し
Option Explicit

Sub SampleCode1()
    Dim Data As New Hogehoge
    Dim i As Long
    
    
    'Addメソッドに対し、Array関数で配列を作って渡せばよいです
    Call Data.Add(Array("A01", "B05", "C001", "D1", "なんとなくだめ"))
    Call Data.Add(Array("A01", "B10", "C002", "D2", "それとなくだめ"))
    Call Data.Add(Array("A01", "B15", "C003", "D3", "そこはかとなくだめ"))
    Call Data.Add(Array("A01", "B20", "C004", "D4", String(256, "■")))


    '一つ一つのデータにアクセスしたい場合はItemメソッドで
    '通常の二次元配列のようにアクセスするとよいです。
    For i = 0 To Data.Last
        Debug.Print Data.Item(0, i) & Data.Item(1, i) & Data.Item(2, i)
    Next
    
    
    '出力する際、いちいち領域を指定する必要はありません。
    '出力したいシートのオブジェクトと左上のセルを指定するだけで十分です。
    Call Data.Output(ThisWorkbook.ActiveSheet, "B3")
    
    '連続してコードを実行して出力する場合、前回出力したデータが残ってしまうので
    '出力時に該当する行より後ろの行をClearしたい時は、事前に列数を指定すると削除してくれます。
    Data.DeleteColumn = 5
    Call Data.Output(ThisWorkbook.ActiveSheet, "B3")
    
    'Clearメソッドで初期化されます。
    Call Data.Clear
    
    '一元配列ならいちいちArray関数にいれる必要はありません。
    Call Data.Add("abc")
    Call Data.Add("def")
    Call Data.Add("ghi")
    
    Call Data.Output(ThisWorkbook.ActiveSheet, "A10")
End Sub

2012年12月23日日曜日

[ExcelVBA] 配列の値をループを使わずに計算する


アドベントカレンダー 23日目

CSVなんかでデータを取得して合計値でも求めたいなぁ
なんてことはまれにあるとおもいますが、
ループでぐるぐる回して合計値を出すのはちょっと面白みにかけるので
こんな方法はどうですか?
という小ネタです。
(ネタですよ・・・)
Option Explicit

Sub SampleCode()
    Dim Data As Variant

    'データは何かしらの方法で受け取るとして…ここではArrayで作っちゃいます
    Data = Array(30, 29, 58, 38, 10, 38, 26, 65, 93)

    '足し算をする
    Debug.Print Evaluate(Join(Data, "+"))
End Sub

足し算記号で配列をJoinして
" 30+29+58+38+10+38+26+65+93"
という文字列を作り上げ、Evaluateで文字列を評価してあげるだけです。

単にEvaluateを使いたかっただけなんですけどねぇ…

ちなみに、Evaluate関数は
Evaluate("A1")でアクティブのA1の値を取得します。
特に使い道はないですね。

2012年12月22日土曜日

[ExcelVBA] ワークシート関数を増やす


アドベントカレンダー 22日目

マクロばかりに目がいくとどうも忘れがちになるというか
こういう使い方をしたい時がないからなのか登場場面が少ないきがしますが
マクロに書いたPublicなFunctionはワークシート関数から使えますよね。

例) BMIを求める関数を実装する
BMIの式は
BMI = 体重 ÷ 身長 ÷ 身長 ですね
Option Explicit

Function BMI(ByVal Height As Double, ByVal Weight As Double) As Double

    BMI = Weight / Height ^ 2

End Function

これでワークシート内で=BMI(1.7, 60)の
ように記載すると値が返ってきます。

何かしらの公式など、関数化してワークシート内で使いまわすなんてやり方は便利よいですね。
自分の場合、力学系の式を登録して、簡易的なチェックとして使っていたりしましたね。

もちろん、関数の挿入[fx]で確認してみても他の関数と同様に示されます。


ただ・・・
説明がないといまいちそっけないですね。

とういうわけで、ヘルプを追加してみます。
Option Explicit

Function BMI(ByVal Height As Double, ByVal Weight As Double) As Double

    BMI = Weight / Height ^ 2

End Function


Sub OwnFunctionHelp()

    Application.MacroOptions Macro:="BMI", _
    Description:="引数は以下の通りです。" & vbLf & "Height:身長[m], Weight:体重[kg]"

End Sub
OwnFunctionHelpを実行すると説明が表示されるようになります。



あくまでもマクロを実行することで追加されるのでExcelが開いたら実行されるようにしておく必要があります。
標準モジュールならAuto_Open関数を作りそこから呼び出すようにするか
ThisWorkbookならWorkbook_Open関数から呼び出すようにする必要があります。

2012年12月21日金曜日

[ExcelVBA] 再帰処理 - QuickSort


アドベントカレンダー 21日目

前日に引き続き、再帰処理の例をあげてみます。
今度はQuickSortです。
この例では、配列のデータは整数のみ限定ということで書いています。

Option Explicit

Sub QuickSort(ByRef Data As Variant, Optional ByVal S As Integer = 0, Optional ByVal E As Integer = -1)
    Dim BaseValue  As Long
    Dim ChangeData As Long
    Dim TempEnd    As Long
    Dim TempStart  As Long
    
    If E = -1 Then E = UBound(Data)
    
    '基準の値を決定(ひとまず中間の要素値を基準とする)
    BaseValue = CLng(Data(Int((S + E) / 2)))

    TempStart = S
    TempEnd = E
    Do
        '基準値より大きい値の要素を探す
        '該当するものがなければ基準値の要素でStopする
        Do While CLng(Data(TempStart)) < BaseValue
            TempStart = TempStart + 1
        Loop
        
        '基準値より小さい値の要素を探す
        '該当するものがなければ基準値の要素でStopする
        Do While BaseValue < CLng(Data(TempEnd))
            TempEnd = TempEnd - 1
        Loop
    
        'Start側の要素数がEnd側の要素数と一致した場合
        '入れ替える物がなかったためこのループは終了とする
        If TempEnd <= TempStart Then Exit Do
    
        'データを入れ替える
        ChangeData = Data(TempStart)
        Data(TempStart) = Data(TempEnd)
        Data(TempEnd) = ChangeData
        
        '範囲をそれぞれ一つずつ狭める
        TempStart = TempStart + 1
        TempEnd = TempEnd - 1
    Loop
    
    '状況によってさらにソートを続ける
    If S < TempStart - 1 Then Call QuickSort(Data, S, TempStart - 1)
    If TempEnd + 1 < E Then Call QuickSort(Data, TempEnd + 1, E)
End Sub


Sub SampleCode()
    Dim Data As Variant
    
    Data = Array(34, 96, 43, 78, 35, 69, 6, 3, 50, 34, 55, 44)
    
    Call QuickSort(Data)
    Debug.Print Join(Data, ",")
End Sub
ただし、QuickSortは安定ソートでないため 一次元配列に対してしか使うことができず 二次元配列には別のソートアルゴリズムを使う必要があります。

2012年12月20日木曜日

[ExcelVBA] 再帰処理 - サブフォルダ内のファイル


アドベントカレンダー 20日目

再帰処理といえば、ある一定の処理が繰り返し
樹木の枝のように発生する場合に使う処理ですね。

例えば、サブフォルダ内のファイルを検索する場合



該当するファイルを探すという行為をフォルダを見つけるたびに1階層下に移動し繰り返しているだけですよね。
こういったパターンの時に使えますね。
他の例だと、クイックソートとかも使いどころですよね。

まずは、再帰処理を使う必要がないくらい単純な例を使って実際に使ってみましょう。
では、10カウントダウンする関数を考えてみます。

普通に書くと、以下のように書けば実現できます。
Option Explicit

Sub SampleCode()
    Call CountDown(10)
End Sub

Sub CountDown(ByVal num As Integer)
    Dim i As Integer
    
    For i = num To 1 Step -1
        Debug.Print i
    Next
End Sub

これと同様のことを再帰処理で実現すると…
Option Explicit

Sub SampleCode()
    Call CountDown(10)
End Sub

Sub CountDown(ByVal num As Integer)
    Debug.Print num
    If 1 < num Then Call CountDown(num - 1)
End Sub
シンプルですねぇ。 これは、カウントするという行為を10回繰り返していて 値だけが変わっているということに着目して書き換えたものです。 ただ、fot文で簡単にかけちゃうといまいち再帰処理のメリットを感じませんね そんなわけで、実際にサブフォルダ内のファイルを探すプログラムあたりを 書いてみると再帰処理のよさを感じられると思うので実際に書いてみるとよいと思います。 参考までに、自分が作成した関数を掲載しておきます。
Option Explicit

Function GetSubFolder(ByRef FilePaths As Object, ByVal CurrentFolder As String, Optional ByVal NameRule As String = "*")
    Dim FileName As String
    Dim Folder   As Variant
    Dim Folders  As Object
    Dim FSO      As Object
    Dim TopFlag  As Boolean
    
    '** Objectの生成 **
    Set FSO = CreateObject("Scripting.FileSystemObject")
    
    '存在しないパスのときは、関数を抜ける
    If Not FSO.FolderExists(CurrentFolder) Then Exit Function
    
    '該当するファイルの取得
    FileName = Dir(CurrentFolder & "\" & NameRule)
    
    Do While FileName <> ""
        
        FilePaths.Add CurrentFolder & "\" & FileName, FileName
        FileName = Dir()
    
    Loop

    'サブフォルダの処理
    Set Folders = FSO.GetFolder(CurrentFolder)
    
    'サブフォルダを一つずつループで処理する
    For Each Folder In Folders.SubFolders
        
        'さらにその下の階層を調べる
        Call GetSubFolder(FilePaths, Folder.Path, NameRule)
    
    Next

    Set FSO = Nothing
    Set Folders = Nothing
End Function

'実際に使ってみる
Sub SampleCode()
    Dim CurrentFolder As String
    Dim FilePath      As Variant
    Dim FilePaths     As Object
    
    '** Objectを生成 **
    Set FilePaths = CreateObject("Scripting.Dictionary")
    
    '引数
    '第一引数:見つかったパスを格納するDictionary
    '          Dictionaryはキーにフルパス、値にファイル名
    '第二引数:検索開始のフォルダパス
    '第三引数:Dir関数同様に探しだすファイル名をアスタリスクを使って指定。
    
    'ファイルを探し出すフォルダパス
    CurrentFolder = "C:\Documents and Settings\Owner\My Documents\excel\test"
    
    '検索開始
    Call GetSubFolder(FilePaths, CurrentFolder, "*")

    '検索結果を出力
    For Each FilePath In FilePaths
        Debug.Print "フルパス:" & FilePath
'        Debug.Print "ファイル:" & FilePaths.Item(FilePath)
    Next
    
    Set FilePaths = Nothing
End Sub

2012年12月19日水曜日

[ExcelVBA] テキストボックスの連動


アドベントカレンダー 19日目

あるシートにテキストボックスを用意しファイルパスなど設定事項を
入力するよう指定していたら別のシートからも変更できるように
お願いできますか?と、言われたとします。

確かに…
設定だけ、あるシートにまとめておくと管理はしやすいものの
いちいち、そこに移動して入力し、また戻ってくるってのは
いささかめんどくさく感じる時もありますよね。

そういった場合、テキストボックス同士どちらで変更しても
同じ情報となるように細工したくなりますよね。
さてどうします?

具体例として以下のケースに出くわしたとします。
(あくまでも例です)

シートは3つあります。
Aシートにはテキストボックスが3つ。
Bシートにはテキストボックスが2つ。
Cシートにはテキストボックスが1つ。

テキストボックス3については、3つのシートどこからでも操作がしたく
それぞれ同期がとれていてほしいわけです。
テキストボックス2は、2つのシートから操作がしたく
それぞれ同期がとれていてほしいわけです。


何も考えずにストレートにコードを書くとこうなりますね。
'Sheet Aのモジュール
Private Sub TextBox2_Change()
    ThisWorkbook.Sheets("B").TextBox2.Text = Me.TextBox2.Text
End Sub

Private Sub TextBox3_Change()
    ThisWorkbook.Sheets("B").TextBox3.Text = Me.TextBox3.Text
    ThisWorkbook.Sheets("C").TextBox3.Text = Me.TextBox3.Text
End Sub


'Sheet Bのモジュール
Private Sub TextBox2_Change()
    ThisWorkbook.Sheets("A").TextBox2.Text = Me.TextBox2.Text
End Sub

Private Sub TextBox3_Change()
    ThisWorkbook.Sheets("A").TextBox3.Text = Me.TextBox3.Text
    ThisWorkbook.Sheets("C").TextBox3.Text = Me.TextBox3.Text
End Sub


'Sheet Cのモジュール
Private Sub TextBox3_Change()
    ThisWorkbook.Sheets("A").TextBox3.Text = Me.TextBox3.Text
    ThisWorkbook.Sheets("B").TextBox3.Text = Me.TextBox3.Text
End Sub

素直にベタ書きしたところです。
今後拡張された場合、どのテキストボックスがどうなってるか
関係を確認した上でコードを追加する必要が生じます。

そんな面倒臭い書き方はしたくないのでもっと楽するには以下のように書きます。
'Sheet Aのモジュール
Private Sub TextBox2_Change()
    Call AllSheetTextBoxChange("TextBox2", TextBox2.Value)
End Sub

Private Sub TextBox3_Change()
    Call AllSheetTextBoxChange("TextBox3", TextBox3.Value)
End Sub


'Sheet Bのモジュール
Private Sub TextBox2_Change()
    Call AllSheetTextBoxChange("TextBox2", TextBox2.Value)
End Sub

Private Sub TextBox3_Change()
    Call AllSheetTextBoxChange("TextBox3", TextBox3.Value)
End Sub


'Sheet Cのモジュール
Private Sub TextBox3_Change()
    Call AllSheetTextBoxChange("TextBox3", TextBox3.Value)
End Sub

'↑ある関数に対し所定の書き方を繰り返すのみですね。

'標準モジュール
Sub AllSheetTextBoxChange(ByVal ControlName As String, ByVal Text As String)
    On Error Resume Next '存在しないテキストボックス名の可能性を考慮
    
    ThisWorkbook.Sheets("A").OLEObjects(ControlName).Object.Value = Text
    ThisWorkbook.Sheets("B").OLEObjects(ControlName).Object.Value = Text
    ThisWorkbook.Sheets("C").OLEObjects(ControlName).Object.Value = Text
    
    On Error GoTo 0
End Sub
今後拡張したい時は、AllSheetTextBoxChangeにシート名を追加するのみです。
以上

2012年12月18日火曜日

[ExcelVBA] Debug.Printで200回以上使っても見切れない方法


アドベントカレンダー 18日目

さて、そろそろネタも少なくなってきたので
だれが使っているんだろう・・・
みたいなネタを一つ。

Debug.Printでイミディエイトウィンドウに出力した際
200行を超えると先頭の行から消えて行くため、
それ以上を一度に表示することはできません。

厳密には、199回+改行で200行となるので199回出力したらその次から最初の方から削除されることになります。

Option Explicit

Sub SampleCode()
    Dim i As Integer
    
    For i = 1 To 199
        Debug.Print i
    Next
End Sub
↑これが限界?

ちょっと悔しいですね。
じゃ、それ以上表示しましょ。

Option Explicit

Sub SampleCode()
    Dim i As Integer
    
    For i = 1 To 300
        Debug.Print i;
    Next
End Sub

これで楽勝ではいりましたね。
Debug.Printの最後尾に ; をつけると改行されません!
以上。

2012年12月17日月曜日

[ExcelVBA] ショートカット


アドベントカレンダー 17日目

ショートカットといえば、いろんな物が用意されており
その人その人の操作の仕方に応じて便利よいものを使えばいいと思います。

そんな中でも、やっぱりこれ便利だよね。
ってのをいくつかめもめも。

■セルのところで
・Ctrl + ; → 日付を表示
・Ctrl + : → 時間を表示
これは、かなり有名ですよね。
よくつかいます。


■VBEのところで
・       F8 → ステップ イン
・   Shift + F8 → ステップ オーバー
・Ctrl + Shift + F8 → ステップアウト
マクロのステップ実行に関するショートカットです。
これも覚えておくと便利ですよね。


■VBEのところで
・Ctrl + スペース → 入力候補を表示。ただし、候補が一つの時は候補を表示することなく残りの単語を補う

入力補助が表示されている時にミスタッチしてdeleteしたりして
打ち直しした時に、入力補助が表示されなくなることがあるので
そういった時は、Ctrl + スペースで補ってあげるとよいですね。


2012年12月16日日曜日

[ExcelVBA] コントロールをRGBで指定


アドベントカレンダー 16日目

ユーザーコントロールの背景色ってぱっとしないですよねぇ。
極めて、Excel的で、面白みがない。。。


でも、RGBで指定も可能なんでRGBで指定しちゃいましょ。



試しに白を指定してみると、上のような結果が得られます。
白はRGB255,255,255を16進数に変換するとFF,FF,FFとなるので
どうやら&H00 と & の間にRGBの値を記載すればよいということのようです。
ただし、左側からBGRの順番で指定する必要があります。

実際にやってみましょう。
下のような色を指定したい時で考えます。


255 と 128ですね。
16進数に変換しましょう。
電卓の関数電卓モードで計算するもよいし、
イミディエイトウィンドウで計算するのでもよいですよね。
お好きな方法で計算しちゃってくださいな。



FF と 80ですね。
では指定してみます。

B→G→Rの順番で指定するので80→80→FFですね



これでお好きな色が指定できますね。


2012年12月15日土曜日

[ExcelVBA] すけすけあぷりけーしょん


アドベントカレンダー 15日目

いやー
ディスプレイが小さいと表示できる面積が小さいから
どうしてもあれもこれも見たいって時に困りますよねぇ。

じゃ、透過すればいいじゃない!
ということで、今日のネタは前日のWindowsAPIの
ハンドル取得を利用して任意のアプリを透明化するあぷりです。

簡単なデモでも紹介します。
まず、電卓を2つ立ち上げスケルトンアプリを立ち上げます。
そして、検索欄に"電卓"と入力してEnterを押します。
この検索欄は、ウィンドウ名を指定します。
簡単な正規表現込みで指定可能です。

するとヒットしたものがリストとして表示されます。


透明にしたい方を選択して、透明にしたい割合(右上のボックス)を0〜100で指定します。
0が透明で100が不透明です。


<30%にした場合>

Excel自体を半透明にすることも…



スケルトンアプリで遊びたい時はこちらからどーぞ
https://docs.google.com/open?id=0B_9e7wIj6Kvua0dtMVhoWHJfN0E

2012年12月14日金曜日

[ExcelVBA] ウィンドウハンドルを取得する - WindowsAPI


アドベントカレンダー 14日目

クラス名がわかっていれば、FindWindow関数でも使ってハンドル取得すりゃいい話なんですが
わからん時は、いちいちEnumWindows関数で全ハンドル取得してウィンドウ名を取得して
該当するハンドルを見つけりゃいいわけですが、ちょいとめんどくさいから関数作ってしまいましょ。

関数の仕様としては、欲しいウィンドウハンドルの名前を指定します。
指定の仕方は正規表現で指定できます。
返り値は、DictionaryのObjectが返ってきます。

Dictionaryの構造は、ハンドルをキーに値にDicitionaryを持ちます。
そのDicitionaryは
ClassName という文字列をキーにクラス名を値に
WIndowName という文字列をキーにウィンドウ名を値に持ちます。

(Dictionary) = Hwnd => (Dictionary)
                                       "ClassName"     => [クラス名],
                                       "WIndowName" => [ウィンドウ名]
 

コードは以下の通りです。
Option Explicit

Private Declare Function EnumWindows Lib "user32.dll" (ByVal lpEnumFunc As Long, lParam As Long) As Long
Private Declare Function GetClassName Lib "user32" Alias "GetClassNameA" _
       (ByVal hwnd As Long, ByVal lpClassName As String, ByVal nMaxCount As Long) As Long
Private Declare Function SendMessageStr Lib "user32.dll" Alias "SendMessageA" _
       (ByVal hwnd As Long, ByVal MSG As Long, _
        ByVal wParam As Long, ByVal lParam As String) As Long

Private Const WM_GETTEXT = &HD
Private Const WM_GETTEXTLENGTH = &HE

Private DicHwnd As Object
Private RegWindowTitle As Object

Public Function GetHwnd(ByVal RegPattern As String) As Object
    Dim Ret As Long
    
    'FileNameに一致するハンドルをキーにタイトルを値に格納する変数
    'CallBack関数とやりとりするのでGlobal領域で宣言しておく
    Set DicHwnd = CreateObject("Scripting.Dictionary")
    Set RegWindowTitle = CreateObject("VBScript.RegExp")
    
    RegWindowTitle.Global = False
    RegWindowTitle.IgnoreCase = True
    RegWindowTitle.Pattern = EscapeRegString(RegPattern)

    '必ずエラーになるので止まらないよう処理しておく
    On Error Resume Next
    Ret = EnumWindows(AddressOf EnumWindowsProc, 0)
    On Error GoTo 0 '問題の部分を過ぎたのでエラー処理をもとに戻す
    
    Set GetHwnd = DicHwnd
    Set DicHwnd = Nothing
End Function

Private Function EnumWindowsProc(ByVal hwnd As Long) As Long
    Dim ClassName   As String
    Dim length      As Integer
    Dim Ret         As Integer
    Dim str         As String
    Dim WindowTitle As String
    
    EnumWindowsProc = 1
    
    'ハンドルに該当する文字列数を取得(注:Null文字は含まない)
    length = SendMessageStr(hwnd, WM_GETTEXTLENGTH, 0, 0)
    length = length + 1 'Null文字分1つ増やす
    
    'Null文字分一つ多く領域を確保
    str = String(length, vbNullChar)
    
    'Null文字も含め受取る
    Ret = SendMessageStr(hwnd, WM_GETTEXT, length, str)
    
    '文字列を出力(NULL文字分1つ削って出力)
    WindowTitle = Left(str, Len(str) - 1)
    
    '取得したウィンドウ名がGetHwndの引数に指定された正規表現がマッチしたら取得しておく
    If RegWindowTitle.Test(WindowTitle) Then
        'クラス名が何文字か不明なので大きめの領域を確保しておく
        ClassName = String(255, vbNullChar)
        Ret = GetClassName(hwnd, ClassName, 255)
        ClassName = Replace(ClassName, vbNullChar, "")
        
        DicHwnd.Add CStr(hwnd), CreateObject("Scripting.Dictionary")
        DicHwnd(CStr(hwnd)).Add "ClassName", ClassName
        DicHwnd(CStr(hwnd)).Add "WindowName", WindowTitle
    End If
    
End Function

Private Function EscapeRegString(ByVal RegHwnd As String)
    RegHwnd = Replace(RegHwnd, "(", "\(")
    RegHwnd = Replace(RegHwnd, ")", "\)")
    RegHwnd = Replace(RegHwnd, ".", "\.")
    RegHwnd = Replace(RegHwnd, "[", "\[")
    
    EscapeRegString = RegHwnd
End Function


こんな感じで値が取れます。
メモ帳を2つ起動した状態で、メモ帳のハンドルを取得したいとします。
Option Explicit

Sub SampleCode()
    Dim hwnd     As Variant
    Dim HashHwnd As Object
    Set HashHwnd = GetHwnd("メモ帳$")
    
    For Each hwnd In HashHwnd
        Debug.Print "ハンドル:" & hwnd
        Debug.Print "クラス名:" & HashHwnd(hwnd)("ClassName")
        Debug.Print "タイトル:" & HashHwnd(hwnd)("WindowName")
        Debug.Print "---"
    Next
End Sub

実行結果

ハンドル:197350
クラス名:Notepad
タイトル:無題 - メモ帳
---
ハンドル:262876
クラス名:Notepad
タイトル:無題 - メモ帳
---


2012年12月13日木曜日

[ExcelVBA] ようこそ画面の名前


アドベントカレンダー 13日目

掲示板のような書き込み式のプログラムを作った際に
名前を自動で指定したいなんてことありますよねぇ。

会社なんかだと、ログインユーザー名がアルファベットで[苗字の頭文字]_[名前]
そして、ようこそ◯◯ って画面で漢字の名前が指定されていたりしますよね。
VBAからだと手っ取り早く取得できるのが、ログインユーザー名の方で下記で取得できます。

CreateObject("WScript.Network").UserName

ログとして取得するならまだしも、利用者同士が目にするとこで利用するなら
ようこそ◯◯画面で表示されるFullNameの方を取得したいものです。
FullNameのデータを取得する方法はいくつかあるのですが
セキュリティの関係で取得できないものがあったりするので
自分の環境で取得できたコードをのっけておきます。
他の方法、ネットで検索したらボロボロでてくるのでそちらにおまかせしておきます。

ここで紹介するのは、WMIを使った方法です。
Option Explicit

Sub main()
    Dim FullName As String
    Dim Locator  As Object
    Dim Service  As Object
    Dim QfeSet   As Object
    Dim Qfe      As Variant
    Dim UserName As String
    
    '** Objectの生成 **
    Set Locator = CreateObject("WbemScripting.SWbemLocator")
    Set Service = Locator.ConnectServer
    Set QfeSet = Service.ExecQuery("Select * From Win32_NetworkLoginProfile")

    UserName = CreateObject("WScript.Network").UserName

    For Each Qfe In QfeSet
        '名前にログイン名が使われているオブジェクトのFullNameを取得をしたい
        If 0 < InStr(Qfe.Name, UserName) Then
            
            FullName = Qfe.FullName
        
        End If
    
    Next

    MsgBox FullName

    Set QfeSet = Nothing
    Set Service = Nothing
    Set Locator = Nothing
End Sub

2012年12月12日水曜日

[ExcelVBA] Gotoを使おう!


アドベントカレンダー 12日目

みなさーん。Goto使ってますかぁ〜♪
はい。じゃんじゃん使ってますね。

いやー便利便利。
Goto使わないとかないですよねぇ。

とかいうと、
スパゲッティーつくりたきゃ、料理人になれよ!
と罵声を浴びてしまいそうになりますが・・・

javascriptならbreak, continue
perlならlast, next
vb.netでさえexit, continue

などがあるというのに…
残念ながらVBAでは次のループへに該当するものがない…
そこでgotoを変わりに使おうよ。
って話ですよ。

するとね。
gotoなんてスパゲッティーの温床!
gotoなんて使わずに表現できるからそうしなさい!
なんて言うわけですよ。

あの・・・
本来のあるべきコードの解釈をねじまげてまで、
gotoを使わないことに必死になることはないと思うんですよね。
そもそも、にっくきはスパゲッティーなコーディングなわけであって
他の言語のループ制御と同等の使い方と限定したならば
特に問題は発生しないと思うんですけどね。

例えば、ファイルを開き1行ずつ処理する場合
開く→1行データ取得→必要データか判定→必要データを所定の処理をして取得
と考えた場合以下のようにかける。
ポイントは、不要データのコードがかかれている部分
必要データのコードが書かれている部分がネストされることなく
はっきりとわかるようになっていること。

Sub SampleCode(ByVal FilePath As String)
    Dim FSO     As Object
    Dim FH      As Object
    Dim OneLine As String
    
    '** Objectの生成 **
    Set FSO = CreateObject("Scripting.FileSystemObject")
    
    
    Set FH = FSO.OPenTextFile(DataFilePath, 1)
    
    Do While Not .AtEndOfStream
        '■1行ずつ取得する
        OneLine = .ReadLine
    
        '■特定の条件を満たさない行はデータとして取得しない
        '空行は次のループへ
        If Trim(OneLine) = "" Then GoTo NextDoLoop
        
        '○○を含む行はヘッダー行なので除外する
        If 0 < InStr(OneLine, "○○") Then GoTo NextDoLoop
        
        '××を含む行はフッター行なので除外する
        If 0 < InStr(OneLine, "××") Then GoTo NextDoLoop
        
        
        '■上記で除外すべきデータを除いているので
        '  ○×△なデータだけになっている
        
        '以下いろんな処理
        
NextDoLoop:
    Loop
    
    FH.Close
    
    Set FH = Nothing
    Set FSO = Nothing
End Sub
可読性を犠牲にしてまで、スパゲッティー呪文に取り憑かれて つかわないようにするのはどうなんだろうねぇ。。 と思う今日この頃でした。

2012年12月11日火曜日

[ExcelVBA] PCロックの妨害


アドベントカレンダー 11日目

コンプラの関係で離席する時は、パソコンにロックをしなさいと口うるさくなる今日この頃。
一定の操作がないとするロックがかかるような設定になったりとコンプラさまさまなわけですが…

ときおり、一部の人だけコードが動かないなんてことがおきるもんだから
その人の環境で調査するため、ログインされた状態で引き渡してもらうのですが
上記理由から、すぐにロックがかかったりするわけですよね。
もちろん、システムなんていじれないようにされてるので、
そうなると、なんかのキーを送信すりゃいいじゃん!
となるのでこうなった。
Option Explicit

Sub SampleCode()
    Dim WSH As Object
    Set WSH = CreateObject("Wscript.Shell")
    
    Do
        WSH.SendKeys "^"
        DoEvents
    Loop
    
    Set WSH = Nothing
End Sub

ひとまず、Ctrlを送信。
これだけでも、十分なんだけど、
これだと、実行しているのか止まっているのか不明なので
もうちょっと視認性を高めるとして、オートシェイプを追加して…
ボタンでスタート/ストップできるようにしてみると。
Option Explicit
Private Declare Sub Sleep Lib "Kernel32" (ByVal dwMilliseconds As Long)

Dim Flag As Boolean
Sub ToggleButton()
    Dim ToggleName As String
    ToggleName = Shapes("Button").TextFrame.Characters.Text
    
    If ToggleName = "スタート" Then
        Shapes("Button").TextFrame.Characters.Text = "ストップ"
        Flag = False
        Call Loooooop
    Else
        Shapes("Button").TextFrame.Characters.Text = "スタート"
        Flag = True
    End If
End Sub


Sub Loooooop()
    Dim WSH As Object

    '** Objectの生成 **
    Set WSH = CreateObject("Wscript.Shell")

    Flag = False

    Do
        If Flag Then Exit Do
        WSH.SendKeys "^"
        Range("B1") = Time
        DoEvents
        Sleep (100)
    Loop
    
    Set WSH = Nothing
    MsgBox "停止"
End Sub

2012年12月9日日曜日

[ExcelVBA] 省略記法


アドベントカレンダー 9日目

連想配列好きな人は、Dictionaryオブジェクトをガンガン使っているとおもいますが
DictionaryオブジェクトのItemは省略して書くことができますよ。

Option Explicit

Sub SampleCode()
    Dim DicObj As Object
    Set DicObj = CreateObject("Scripting.Dictionary")

    DicObj.Add "First", CreateObject("Scripting.Dictionary")
    DicObj.Item("First").Add "Second", CreateObject("Scripting.Dictionary")
    DicObj.Item("First").Item("Second").Add "Third", CreateObject("Scripting.Dictionary")
    DicObj.Item("First").Item("Second").Item("Third").Add "Fourth", CreateObject("Scripting.Dictionary")
    DicObj.Item("First").Item("Second").Item("Third").Item("Fourth").Add "Fifth", "ok"
    
    Debug.Print DicObj.Item("First").Item("Second").Item("Third").Item("Fourth").Item("Fifth")
    Set DicObj = Nothing
End Sub

これをItemを省略して書くと…
Option Explicit

Sub SampleCode()
    Dim DicObj As Object
    Set DicObj = CreateObject("Scripting.Dictionary")

    DicObj.Add "First", CreateObject("Scripting.Dictionary")
    DicObj("First").Add "Second", CreateObject("Scripting.Dictionary")
    DicObj("First")("Second").Add "Third", CreateObject("Scripting.Dictionary")
    DicObj("First")("Second")("Third").Add "Fourth", CreateObject("Scripting.Dictionary")
    DicObj("First")("Second")("Third")("Fourth").Add "Fifth", "ok"
    
    Debug.Print DicObj("First")("Second")("Third")("Fourth")("Fifth")
    Set DicObj = Nothing
End Sub

見慣れない人には、わかりづらいかもしれませんが、
深い階層になった時横に長くなってみづらいことは
まぁまぁあるんで、そういう時とかのためにも.Itemを省略した
書き方に慣れてしまうのも一つの手なのかもしれませんね。

ちなみに、Collectionの.Itemは省略すると叱られます。

[ExcelVBA] 背景色の設定


アドベントカレンダー 10日目



見た目ってどうしてます?
ExcelってどうがんばってもMicrosoft的カラーリングから抜け出しにくいですよね。
くすんだ灰色とか、微妙なパステルカラーとか・・・

もっと自由な色を使いたい!
なんて方もいるんではないでしょうか?

となると、あれを使うか…
と思うわけですよね。
あれです。


えっと、Excelのメニューバーの
ツール→オプション→色の…
なんてベタなことはいいませんよ 笑

たまにゃ違った方法をお勧めしてみますか。
では、コントロールツールボックスでも使いましょう。



Microsoft Office Spreadsheetを選択します。
ドラッグしてSpreadsheetコントロールを配置します。

Spreadsheet内のエクセルはデフォルトの状態でも
カラーパレットの数も多く以下のようにため息しかでない
色がいっぱい登録されています。



ユーザー設定を押せばRGBで設定も可能です。

例えば、こんなデザインだったり…


(ごめんなさい。自分にはデザインセンスがないです・・・)

デザインセンスのある方ならもっと綺麗に作り込めそうですねっ!


そうそう。
ここまでは冗談話として、
好きな色を設定したい時は、
RangeObj.Interior.Color = RGB(R値, G値, B値)
RangeObj.Font.Color = RGB(R値, G値, B値)
とすればいよ。

あ、それからSpreadSheetってセル内で改行ができないみたい・・・

2012年12月8日土曜日

[ExcelVBA] 浮動小数点問題


アドベントカレンダー 8日目

たまには、こんなネタも。
45.2 - 38.8 はいくつでしょうか?
では、計算してみましょう。
Option Explicit

Sub SampleCode()
    Dim a As Double
    Dim b As Double
    
    a = 45.2
    b = 38.8
    
    Debug.Print a - b
End Sub

結果は
6.40000000000001
です。

こんなに簡単んそうな計算結果でも誤差が出てしまいます。
これは、IEEE 754という数値計算の標準規格を利用しているものなら
全て発生する問題だそうで、詳細を知りたい場合は以下サイトなんかを
眺めるとよいと思います。

第4回 演算誤差の正体

2012年12月7日金曜日

[ExcelVBA] 名前をつけて保存的なことをしたい


アドベントカレンダー 7日目

こんなシチュエーションありませんか?
それは、マクロを実行した結果を別名で保存したい時。
しかも、ユーザー自身が保存場所を選択して、そこへ保存する場合です。


ファイルの中身をうっかり変更されてしまうのを防ぐために
読み取り専用にして使っているケースは多いと思うのですが
マクロで作成したデータを保存してもらうために
わざわざ、ユーザー自身がメニューバーのファイルにある
名前をつけて保存を選択して保存をしてもらうのはちょっぴりいまいち感でいっぱいですよね!

そこは、「保存する」ボタンを用意してあげて
プログラム側で処理してあげましょう。

Option Explicit

Sub SampleCode()
    Dim FileName As String
    
    FileName = Application.GetSaveAsFilename
    
    If FileName <> "False" Then
        ActiveWorkbook.SaveAs FileName:=FileName
    End If
    
End Sub
6行目のApplication.GetSaveAsFilenameを実行したら「名前を付けて保存」の画面が立ち上がります。
ここでキャンセルを押されるとFalseの文字列が返してきます。
なので、ファイル名を指定して保存を押した場合は、フルパスが返ってきます。
よって、フルパスの場合、ドライブ名で始まるか、ネットワーク上なら¥¥で始まるため
"False"だけの場合、キャンセルを押されたと判断できます。

8行目は、キャンセルだった場合、別名保存をしないための処理を行なっています。
9行目は、別名保存しているだけです。

また、「名前を付けて保存」の初期フォルダ位置を変更したい時は、
InitialFileNameにパスを指定すればOKです。
例えば、c:ドライブ直下の場合は以下の通り。
Option Explicit

Sub SampleCode()
    Dim FileName As String
    
    FileName = Application.GetSaveAsFilename(InitialFileName:="c:\")
    
    If FileName <> "False" Then
        ActiveWorkbook.SaveAs FileName:=FileName
    End If
    
End Sub

ファイルのタイプを指定したい場合は、
FileFilterにファイルのタイプを指定します。
Option Explicit

Sub SampleCode()
    Dim FileName As String
    
    FileName = Application.GetSaveAsFilename(InitialFileName:="c:\", FileFilter:="画像ファイル (*.jpg),*.jpg")
    
    If FileName <> "False" Then
        ActiveWorkbook.SaveAs FileName:=FileName
    End If
    
End Sub

2012年12月6日木曜日

[ExcelVBA] オートシェイプの値を読み書き


アドベントカレンダー 6日目

オートシェイプのテキスト名を取得/変更したい

マクロの記録を使うと以下のようなコードを返してくる。
(いろいろ 略)
    ActiveSheet.Shapes("オートシェイプ名").Select
    Selection.Characters.Text = "aabb"
(いろいろ 略)

つまり、Characters.Textとすると値がRead/Writeできるわけです。
ところが、いちいちSelectしてから操作するのはいまいちなので
Selectを省略してみると、さっそくエラーとなります。

ActiveSheet.Shapes("オートシェイプ名").Characters.Text = "aabb"

Characters.Textは、アクティブなオートシェイプに対してしか、
テキストのRead/Writeはできないようです。

そのため、Characters.Textではなく
TextFrame.Characters.TextであればSelectしておく必要がなくなります。

ActiveSheet.Shapes("オートシェイプ名").TextFrame.Characters.Text

Thisworkbook.ActiveSheet.Shapes("オートシェイプ名").TextFrame.Characters.Text