ユーザーフォームのサイズ変更 ③ 画像を張り付けてみる

先日はスピンボタンで、ユーザーフォームのサイズを段階的に変化させてみた。
infoment.hatenablog.com

今日はこのユーザーフォームに、画像を表示することに挑戦する。
f:id:Infoment:20200125122153j:plain

もともと今回のネタは、画像の中身をユーザーフォームに表示したいというのが切っ掛けだった。そこで今回は、Imageコントロールで画像を表示してみよう。
f:id:Infoment:20200125163328p:plain

画像は決め打ちで準備する。
f:id:Infoment:20200125163438p:plain

Private Sub UserForm_Initialize()
    ' スピンボタンの最大値と最小値を設定。
    SpinButton1.Min = 1
    SpinButton1.Max = 6
    ' 初期値。
    SpinButton1.Value = 3
    
    ' 今回追加。
    Image1.Picture = LoadPicture("C:\Temp\Sample.jpg")
End Sub

イメージのサイズは、ユーザーフォームのサイズに追従して欲しい。そこで、イメージの左上を起点として、10ポイントずつ内側に収まるようにしてみる。
f:id:Infoment:20200125165358p:plain

色々と数値を調整して、このようになった。

Private Sub SpinButton1_Change()
    Static Dict As Scripting.Dictionary
        If Dict Is Nothing Then
            Set Dict = Devisor(12)
        End If

    ' ユーザーフォームのサイズ変更。
    ' 高さは、高さの約71%(1/√2倍)とした。
    ' ※馴染みのあるAサイズ(A3やA4など)の比率。
    UserForm1.Width = unit_width * Dict(SpinButton1.Value)
    UserForm1.Height = UserForm1.Width / 2 ^ 0.5
    
    ' 今回追加。
    With Image1
        .Width = UserForm1.Width - 30
        .Height = UserForm1.Height - 72
        .Top = 40
        .Left = 10
    End With
End Sub

テスト結果は、まずまずと言ったところか。
f:id:Infoment:20200125171940g:plain

明日に続きます。

参考まで。

ユーザーフォームのサイズ変更 ② 画面のn分の1サイズ(約数を求める関数の利用)

先日はユーザーフォームのサイズを、スピンボタンで変更してみた。
infoment.hatenablog.com

今日は、その続きに挑戦する。
f:id:Infoment:20200119102227p:plain

先日は、ユーザーフォームの単位サイズを決め(例 50)、その倍数で横幅を変化させた。しかしもしユーザーフォームを複数枚横に並べるならば、ぴったり画面に収まった方が気持ちいい。

そこでまず、単位サイズをアクティブな画面の十二分の一にしてみた。

Private Property Get unit_width() As Long
' ユーザーフォームの単位幅。
' スピンボタンを押すたびに、この値の倍数で
' ユーザーフォームの幅を変化させる。
    unit_width = ActiveWindow.Width / 12
End Property

単位サイズを12で割ったのは、ピッタリで様々なパターンに並べるため。
 
f:id:Infoment:20200119103109p:plain

スピンボタンが一つずつ変化するごとに、12の約数を上下させたい。
そこで、指定した数値の指定番目の約数を求める関数を作成してみた。

' 約数を求める関数。
' 約数を求めたい関数を1から順に割り、
' 余りが0の値を約数として辞書に格納する。
' ※MicroSoft Scripting Runtime 参照済み。
Private Function Devisor(source_number As Long) As Scripting.Dictionary
    Dim Dict As Scripting.Dictionary
    Set Dict = New Scripting.Dictionary
    Dim i As Long
    Dim Counter As Long: Counter = 1
        For i = 1 To source_number
            If source_number Mod i = 0 Then
                Dict(Counter) = i
                Counter = Counter + 1
            End If
        Next
        Set Devisor = Dict
End Function

スピンボタンの上下に伴うイベントは、値のChangeイベントに集約。

Private Sub SpinButton1_Change()
    Static Dict As Scripting.Dictionary
        If Dict Is Nothing Then
            Set Dict = Devisor(12)
        End If

    ' ユーザーフォームのサイズ変更。
    ' 高さは、高さの約71%(1/√2倍)とした。
    ' ※馴染みのあるAサイズ(A3やA4など)の比率。
    UserForm1.Width = unit_width * Dict(SpinButton1.Value)
    UserForm1.Height = UserForm1.Width / 2 ^ 0.5
End Sub

これにより、初期化部分も簡素化出来た。

Private Sub UserForm_Initialize()
    ' スピンボタンの最大値と最小値を設定。
    SpinButton1.Min = 1
    SpinButton1.Max = 6
    ' 初期値。
    SpinButton1.Value = 3
End Sub

早速、実験してみよう。
f:id:Infoment:20200119103628g:plain

これで、使用環境に合わせてユーザーフォームのサイズを
変更できるようになった。

次回に続きます。

参考まで。

ユーザーフォームのサイズ変更 ~ スピンボタンで操作 ~

ユーザーフォームのサイズを、任意に変更したくなった。
f:id:Infoment:20200117233459p:plain

ユーザーフォームの端っこを摘まんで、びよ~んとサイズ変更できると有難い。
しかし、そのような機能は無いようだ。

そこで、試しにスピンボタンを設けてみた。こんな感じだ。
f:id:Infoment:20200117233627p:plain

' ユーザーフォームの単位高さ。
' スピンボタンを押すたびに、この値の倍数で
' ユーザーフォームの高さを変更させる。
Const unit_width As Long = 50

Private Sub ChangeFormSize()
    ' ユーザーフォームのサイズ変更。
    ' 幅は、高さの約140%(√2倍)とした。
    ' ※馴染みのあるAサイズ(A3やA4など)の比率。
    UserForm1.Height = unit_width * SpinButton1.Value
    UserForm1.Width = UserForm1.Height * 2 ^ 0.5
End Sub

Private Sub SpinButton1_SpinDown()
    Call ChangeFormSize
End Sub

Private Sub SpinButton1_SpinUp()
    Call ChangeFormSize
End Sub

Private Sub UserForm_Initialize()
    ' スピンボタンの最大値と最小値を設定。
    SpinButton1.Min = 1
    SpinButton1.Max = 10
    ' 初期値。
    SpinButton1.Value = 5
    ' 初期値に合わせてユーザーフォームサイズ変更。
    Call ChangeFormSize
End Sub

動きとしては、こんな感じだ。
f:id:Infoment:20200117234254g:plain

悪くないかも。明日に続きます。

参考まで。

With で New すると、Class を Initialize してくれる

先日、このような記事を書いた。
infoment.hatenablog.com

ところが一方で、 With ステートメントで New する場合は、ちゃんと Class を Initialize してくれる(最近知った)。(それにしても、なんて怪しい日本語)。

Sub test()
    With New Class1
        ' 何某かの処理。
    End With
End Sub

f:id:Infoment:20200116223632g:plain

実際は、変数で受けた方が便利な場合が多いので、使う機会は少ないかもしれない。でも、覚えておいて損は無いと思う。

もう一つ最近の個人的流行が、With ステートメントによる変数の省略だ。
例えば、テーブルにレコードを一つ追加して、1列目に数字を入力する場合。

f:id:Infoment:20200116224315p:plain

ここに「ばなな」を追加する時、つい最近まで、このように書いていた。

Sub test()
    Dim Tb As ListObject
    Set Tb = ActiveSheet.ListObjects(1)
    
    Dim ListRow As ListRow
    Set ListRow = Tb.ListRows.Add
        ListRow.Range.Cells(1) = "ばなな"
End Sub

しかし今は、このように書くこともある(場合による)。

Sub test()
    With ActiveSheet.ListObjects(1).ListRows.Add
        .Range.Cells(1) = "ばなな"
    End With
End Sub

可読性(分かり易さ)を損なわない程度なら、これもありかな。

参考まで。

初めて仕事で書いたマクロ(失敗談)

「仕事で初めてマクロを書いたときのことを覚えていますか?」
というご質問をいただいた。そこで、一番古い記憶を辿ってみた。
f:id:Infoment:20200115214542p:plain
「初めて」か否かは、定かでない。ただ、一番古い記憶がこちらだと思う。

数値を小数点第三位に四捨五入して、テキストファイルに書き出す。

工作機械で加工プログラムを作成するとき、プログラムをテキストファイルに書きNC(数値制御装置)に取り込む場合がある。

この時(15年以上前)の或る事案では、加工時の刃物の軌跡が何某かの数式で定まっており、Excelで座標を計算していた。そして担当者から、書き出した連続するXY座標を、小数点第四位以下は邪魔なので切って貰えないかと頼まれた。

そこでマクロに挑戦したのが、一番古い記憶だと思う。

このような数値があるとする。
f:id:Infoment:20200115215442p:plain

これを、例えばこのように書き出してみる。

Sub Test()
    Dim TextFile As String
        TextFile = "C:\Temp\TextFile.txt"
        Open TextFile For Output As #1
        
    Dim i As Long
        For i = 1 To 3
            Print #1, Cells(i, 1)
        Next
        
        Close #1
End Sub

結果、このようなテキストファイルが作成される。
f:id:Infoment:20200115215630p:plain

そこで単純に、書式を下三桁にして書き出してみた。
f:id:Infoment:20200115215733p:plain

だが、結果は同じだった。見た目を変えても中身は変わっていないため、当たり前と言えば当たり前のこと。しかし当時は知識も乏しく(無いに等しく)、ここ以上先に進めなかった。

これをもし今やるとしたら、どうだろう。こんな感じかな。

Sub Test()
    Dim TextFile As String
        TextFile = "C:\Temp\TextFile.txt"
        Open TextFile For Output As #1
        
    Dim i As Long
        For i = 1 To 3
            Print #1, WorksheetFunction.Round(Cells(i, 1), 3)
        Next
        
        Close #1
End Sub

書き出すとき、単純に四捨五入しただけ。こんなにも単純なことが、当時の私にはできなかった。15年前に戻って、自分に教えてあげたい(その他、あんなことや、こんなことも教えておきたい)。しかし、今となっては叶わないこと。

という訳で、↓セピア色っぽくしてみました。
f:id:Infoment:20200115220522p:plain

参考まで。

特定のフォルダにあるExcelファイルを全部開き、各A1の値をコピーして一つのファイルに集約する

特定のフォルダにあるExcelファイルを全て開き、その中身を集約する場面があった。そこで昔を振り返りながら、わがマクロの変遷を辿ってみる。
f:id:Infoment:20200114195030p:plain

例えば、↓ こんな感じでファイルが三つあるとする。
f:id:Infoment:20200114195123p:plain

実際の場面では、毎回何個か分からないだろうが、とりあえず今回は三個とする。
それぞのシートのA1には、食べ物の名前が入力されている。
f:id:Infoment:20200114195424p:plain

昔はとにかく、決め打ちだった。パスも固定で、一つずれれば手直しが必要。

Sub Test_1()
    Workbooks.Open ("C:\Temp\test1.xlsx")
    Range("A1").Copy
    ActiveWorkbook.Close
    
    Range("A1").PasteSpecial
    
    Workbooks.Open ("C:\Temp\test2.xlsx")
    Range("A1").Copy
    ActiveWorkbook.Close
    
    Range("A2").PasteSpecial
    
    Workbooks.Open ("C:\Temp\test3.xlsx")
    Range("A1").Copy
    ActiveWorkbook.Close
    
    Range("A3").PasteSpecial
End Sub

これを活かしつつ修正するとしたら、こんな感じかな。

Sub Test_1()
    Dim i As Long
        For i = 1 To 3
            Workbooks.Open ("C:\Temp\test" & i & ".xlsx")
            Range("A1").Copy
            ActiveWorkbook.Close
            
            Cells(i, 1).PasteSpecial
        Next
End Sub

しかし実際は、毎回こんな都合の良いファイル名であるはずもなく。


時が経ち、Dir関数を覚えた。定番の、Do ~ Loop との組み合わせだ。

Sub Test_2()
    Const FolderPath As String = "C:\Temp"
    Dim FileName As String
        FileName = Dir(FolderPath & "\*.xls*")
        
        Workbooks.Open (FolderPath & "\" & FileName)
        Range("A1").Copy
        ActiveWorkbook.Close False
        Range("A1").PasteSpecial
        
        FileName = Dir
        
        Do While FileName <> ""
            Workbooks.Open (FolderPath & "\" & FileName)
            Range("A1").Copy
            ActiveWorkbook.Close False
            Range("A" & Rows.Count).End(xlUp).Offset(1).PasteSpecial
            FileName = Dir
        Loop
End Sub

流石に各ファイルパスの決め打ちは無くなったものの、未だ全体的に野暮ったい。


そして更に時は流れ現在に至る。今ある知識で、贅の限りを尽くしたのがコチラ。

Sub Test_3()
    ' 画面更新の一時停止。
    ' ※ファイルの数だけ開いたり閉じたりするため。
    Application.ScreenUpdating = False
    
    ' ファイルが保存されたフォルダー。
    Const FolderPath As String = "C:\Temp"
    
    ' ファイル操作用。
    ' MicroSoft Scripting Runtime 参照済み。
    Dim FSO As Scripting.FileSystemObject
    Set FSO = New Scripting.FileSystemObject
    
    ' 値格納用。
    Dim Dict As Scripting.Dictionary
    Set Dict = New Scripting.Dictionary
      
    Dim File As Scripting.File
    ' フォルダー内の各ファイルについて処理。
        For Each File In FSO.GetFolder(FolderPath).Files
        
        ' 拡張子がExcelのものであることを確認する。
        ' ※xls*にすることで、xls,xlsx,xlsm,xlsbの全てに対応。
            If FSO.GetExtensionName(File.Path) Like "xls*" Then
                
                ' リンクの更新要求を避けるため、UpdateLinksはFalseとする。
                ' 編集不要なため、読み取り専用(ReadOnly)で開く。
                With Workbooks.Open(FileName:=File.Path, _
                                    UpdateLinks:=False, _
                                    ReadOnly:=True)
                    
                    ' ファイルパスをキーに、A1の値をアイテムとして辞書に登録。
                    Dict(File.Name) = Range("A1").Value
                    
                    ' 用が済んだファイルを閉じる。
                    .Close False
                End With
            End If
        Next
        
        ' 辞書のアイテムは配列なので、シートに一括貼り付けできる。
        Range("A1").Resize(UBound(Dict.Keys) + 1) = WorksheetFunction.Transpose(Dict.Items)

    ' 画面更新停止の解除。
    Application.ScreenUpdating = True
End Sub

でも実は、PowerQueryを用いれば、マクロ不使用でも同じことができるので、
本当はそれが一番スマートかも。

来年の今頃、同じテーマでもう一度検討してみると、自分の成長度合いが確認
できてよいかも。

参考まで。

RandArray関数っぽいユーザー定義関数で、重複不可の引数を追加

昨日は、RandArray関数っぽく振舞う、ユーザー定義関数を作ってみた。
infoment.hatenablog.com

これの使い処を考えるうち、思いついた。少し手を加えれば、ランダムな並び替えに使用できるのでは?ということで、やってみた。
f:id:Infoment:20200113232131p:plain

ランダム並び替えといっても、実はたいしたことをしていない。
単に、ランダムな整数の重複を禁止しただけ。結果が、ランダムに並べ替えたように見えるだけだったりする。

Function DemiRandArray(r_max As Long, _
                       c_max As Long, _
                       dra_min As Long, _
                       dra_max As Long, _
                       integer_flag As Boolean, _
              Optional duplicate_flag As Boolean = False) As Variant

    ' duplicate_flagがTrueの場合、整数に限り値の重複を不可とする。
    
    ' ランダムに作成した値を格納するための配列。
    Dim arr() As Variant
    ReDim arr(1 To r_max, 1 To c_max)
    
    ' ループ変数:行。
    Dim r As Long
    ' ループ変数:列。
    Dim c As Long
    ' 重複不可の場合に使用する辞書。
    Dim Dict As Object
    Set Dict = CreateObject("Scripting.Dictionary")
    
    ' ランダムに作成した値を一旦格納するための変数。
    Dim temp As Double
        For r = 1 To r_max
            For c = 1 To c_max
                ' Trueの場合、整数のみとする。
                Select Case integer_flag
                    Case True
                        ' Trueの場合、重複不可とする。
                        Select Case duplicate_flag
                            Case True
                                Do
                                    temp = WorksheetFunction.RandBetween(dra_min, dra_max)
                                    If Dict.Exists(temp) = False Then
                                        arr(r, c) = temp
                                        ' キー情報の重複を確認したいだけなので、アイテム不問。
                                        ' なんでもよいので、今回は「1」とした。
                                        Dict(temp) = 1
                                        Exit Do
                                    End If
                                Loop
                            Case False
                                arr(r, c) = WorksheetFunction.RandBetween(dra_min, dra_max)
                        End Select
                    Case False
                        Dim myFlag As Boolean
                            myFlag = False
                        Do While myFlag = False
                            temp = Rnd * (dra_max + 1)
                            If dra_min <= temp And temp <= dra_max Then
                                arr(r, c) = temp
                                myFlag = True
                            End If
                        Loop
                End Select
            Next
        Next
                
        DemiRandArray = arr
End Function

それでは、1~75までの数字を縦に並べてみよう。
まずは、重複可の場合。

Sub Test()
    Dim arr As Variant
        arr = DemiRandArray(75, 1, 1, 75, True)
        Range("A2").Resize(75) = arr
End Sub

ピボットテーブルで集計してみると、欠番が生じていることが分かる。
f:id:Infoment:20200113232548p:plain

次いで、重複不可の場合。

Sub Test()
    Dim arr As Variant
        arr = DemiRandArray(75, 1, 1, 75, True, True)
        Range("A2").Resize(75) = arr
End Sub

1~75までの数が、全て一つずつ生成されている。
f:id:Infoment:20200113232745p:plain

どうやら、上手くいったようだ。

では、これで何をするかというと・・・ビンゴゲームかな。

参考まで。