和風スパゲティのレシピ

日本語でコーディングするExcelVBA

VBA課題002「最も近い色を見つけよう!」回答コード

Excel・VBA総合コミュニティ「Excel-Fun.xls*」にて毎月出題される、
VBAの問題に対する私の回答ソースコードを掲載いたします。


今回掲載するコードは第2回課題共有会の課題でもあった、
最も近い色を見つけよう!」問題です。
VBA課題002「最も近い色を見つけよう!」

今回も読みやすさ重視で書いてみましたので、
よろしければご参考ください(´∀`)

課題の内容

VBAのカラー定数一覧表を元に、
着色された任意の色について最も近いカラー定数を見つけるお題です!
カラー定数一覧


詳細は下記リンク内にある「課題説明」シートをご確認ください。


せっかくなので回答を見る前に皆さんも挑戦してみましょう。

一度自分が作ってから他の人のコードを見ることで、
学習効果が何倍にも高まります。

完成させなくても、出来る範囲で解くだけでも十分効果がありますので、
是非ともトライしてみてください!

課題共有会とは

ExcelVBAに関する1つのお題にみんなで回答し、
各自のコードを解説しあう勉強会です。


他者のコードを見る機会が少ないVBAユーザーにとって、
いろいろな手法・記法に触れることができる貴重な機会です。

もちろん聞き専での参加もできますので、
ご興味がある方はコミュニティを覗いてみてください。


今回の「最も近い色を見つけよう!」に対する私以外の回答コードも公開されています。

これだけたくさんの方のソースコードを見れる機会はなかなかないと思いますので、
是非ともVBAの勉強にお役立ていただければと思います。


◇ 「最も近い色を見つけよう!」の課題ファイルはこちら
Discord:★出題【VBA】#002 最も近い色を見つけよう!


◇ 「最も近い色を見つけよう!」への回答集はこちら
Discord:☆回答【VBA】#002 最も近い色を見つけよう!


※上記はDiscord参加済みの方用のURLです
 未参加の方は下記からご参加ください!
www.limecode.jp

課題の回答ファイル

実際の回答ファイルはこちらをご覧ください。
(和風スパ)【VBA】002_最も近い色を見つけよう!.xlsm

ソースコード

クラスモジュール:ClassRGB操作

Option Explicit

Private リンクするセル_ As Range
Private リンクする図形_ As Shape
Public is文字色をリンクする As Boolean

Private RGB_ As Long
Private R_ As Long
Private G_ As Long
Private B_ As Long

' 書込プロパティ
Property Let RGB(代入値 As Long)
    RGB_ = 代入値
    R_ = RGB_ Mod 256
    G_ = Int(RGB_ / 256) Mod 256
    B_ = Int(RGB_ / 256 / 256)
    
    Call リンクするオブジェクトを着色する
End Property
Property Let R(代入値 As Long)
    R_ = 代入値
    RGB_ = VBA.RGB(R_, G_, B_)
    Call リンクするオブジェクトを着色する
End Property
Property Let G(代入値 As Long)
    G_ = 代入値
    RGB_ = VBA.RGB(R_, G_, B_)
    Call リンクするオブジェクトを着色する
End Property
Property Let B(代入値 As Long)
    B_ = 代入値
    RGB_ = VBA.RGB(R_, G_, B_)
    Call リンクするオブジェクトを着色する
End Property
Property Set リンクするセル(代入値 As Range)
    Set リンクするセル_ = 代入値
    If is文字色をリンクする Then
        RGB = リンクするセル_.Font.Color
    Else
        RGB = リンクするセル_.Interior.Color
    End If
End Property
Property Set リンクする図形(代入値 As Shape)
    Set リンクする図形_ = 代入値
    If is文字色をリンクする Then
        RGB = リンクする図形_.TextFrame.Characters.Font.Color
    Else
        RGB = リンクする図形_.Fill.ForeColor.RGB
    End If
End Property

Sub リンクするオブジェクトを着色する()

    If Not リンクするセル_ Is Nothing Then
        If is文字色をリンクする Then
            リンクするセル_.Font.Color = RGB_
        Else
            リンクするセル_.Interior.Color = RGB_
        End If
    End If
        
    If Not リンクする図形_ Is Nothing Then
        If is文字色をリンクする Then
            リンクする図形_.TextFrame.Characters.Font.Color = RGB_
        Else
            リンクする図形_.Fill.ForeColor.RGB = RGB_
        End If
    End If
    
End Sub

' 読取プロパティ
Property Get RGB() As Long
    RGB = RGB_
End Property
Property Get RGBテキスト() As String
    RGBテキスト = R_ & "," & G_ & "," & B_
End Property
Property Get G() As Long
    G = G_
End Property
Property Get R() As Long
    R = R_
End Property
Property Get B() As Long
    B = B_
End Property
Property Get リンクするセル() As Range
    Set リンクするセル = リンクするセル_
End Property
Property Get リンクする図形() As Shape
    Set リンクする図形 = リンクする図形_
End Property

シートモジュール:WS近似カラー検索

Option Explicit

' セルアドレス
Property Get Cell対象カラー() As Range
    Set Cell対象カラー = Range("B2")
End Property
Property Get Cell対象カラーRGBテキスト() As Range
    Set Cell対象カラーRGBテキスト = Range("C2")
End Property
Property Get Cell対象カラーR() As Range
    Set Cell対象カラーR = Range("D2")
End Property
Property Get Cell対象カラーG() As Range
    Set Cell対象カラーG = Range("E2")
End Property
Property Get Cell対象カラーB() As Range
    Set Cell対象カラーB = Range("F2")
End Property
Property Get Area対象カラーRGB() As Range
    Set Area対象カラーRGB = Range("D2:F2")
End Property
Property Get DataRangeカラー定数一覧() As Range
    Set DataRangeカラー定数一覧 = Me.AutoFilter.Range.Offset(1) _
        .Resize(Me.AutoFilter.Range.Rows.Count - 1)
End Property
Property Get Cell最近似カラー() As Range
    Set Cell最近似カラー = DataRangeカラー定数一覧.Cells(1, CNo近似カラー検索.カラー)
End Property
Property Get Cell最近似XlrgbColor定数() As Range
    Set Cell最近似XlrgbColor定数 = DataRangeカラー定数一覧.Cells(1, CNo近似カラー検索.XlRgbColor定数)
End Property
Property Get Cell最近似ColorConstants定数() As Range
    Set Cell最近似ColorConstants定数 = DataRangeカラー定数一覧.Cells(1, CNo近似カラー検索.ColorConstants定数)
End Property
Property Get SortKey近似スコア() As Range
    Set SortKey近似スコア = Cells(1, CNo近似カラー検索.近似スコア)
End Property

' RGB値の直接取得・代入
Property Get 対象RGB値() As Long
    対象RGB値 = Cell対象カラー.Interior.Color
End Property
Property Let 対象RGB値(代入値 As Long)
    Cell対象カラー.Interior.Color = 代入値
    Call 対象カラーセルのRGB値を計算して表示する
End Property

' RGB値の計算・表示
Private Sub 対象カラーセルのRGB値を計算して表示する()

    With New ClassRGB操作
        Set .リンクするセル = Cell対象カラー
        Cell対象カラーR = .R
        Cell対象カラーG = .G
        Cell対象カラーB = .B
        DataRangeカラー定数一覧.Columns(1).Interior.Color = .RGB
    End With

End Sub

' セルの編集時
Private Sub Worksheet_Change(ByVal Target As Range)

    Call エクセルの自動更新を停止する(False, False, True)

    If Not Intersect(Target, Area対象カラーRGB) Is Nothing Then
        
        With New ClassRGB操作
            Set .リンクするセル = Cell対象カラー
            .R = Cell対象カラーR
            .G = Cell対象カラーG
            .B = Cell対象カラーB
            DataRangeカラー定数一覧.Columns(1).Interior.Color = .RGB
        End With
    
    End If

    Call エクセルの自動更新を開始する

End Sub

' ダブルクリック時
Private Sub Worksheet_BeforeDoubleClick(ByVal Target As Range, Cancel As Boolean)
    Dim R As Long: R = Target.Row
    Dim C As Long: C = Target.Column
    Dim Adrs As String: Adrs = Target.Address(False, False)
    
    If C = CNo近似カラー検索.カラー _
    And R >= DataRangeカラー定数一覧.Row Then
        
        Call ★学習データをセットする(R)
        Cancel = True
        
    End If

End Sub

標準モジュール:Pr_近似色カラー定数の取得

Option Explicit

Public Enum CNo近似カラー検索
    対象カラー = 1
    カラー
    RGBテキスト
    R
    G
    B
    Max
    Min_Max
    Min
    近似スコア
    差_R
    差_G
    差_B
    差_MAX
    差_Min_Max
    カラー名称
    XlRgbColor定数
    ColorConstants定数
End Enum

' 近似カラーの取得
Function Get近似カラー定数(RGB値 As Long) As Long

    With WS近似カラー検索
        .対象RGB値 = RGB値
        .DataRangeカラー定数一覧.Sort .SortKey近似スコア
        Get近似カラー定数 = WS近似カラー検索.Cell最近似カラー.Interior.Color
    End With

End Function

' 近似カラーの取得
Function Get近似カラー定数名(RGB値 As Long) As String

    With WS近似カラー検索
        .対象RGB値 = RGB値
        .DataRangeカラー定数一覧.Sort .SortKey近似スコア
        Get近似カラー定数名 = IIf(WS近似カラー検索.Cell最近似ColorConstants定数.Value <> "" _
                                , WS近似カラー検索.Cell最近似ColorConstants定数.Value _
                                , WS近似カラー検索.Cell最近似XlrgbColor定数.Value)
    End With

End Function

標準モジュール:Pr_学習データの生成

Sub ★テスト色を乱数生成する()

    Call Get近似カラー定数(RGB(Fx.RandBetween(0, 255), Fx.RandBetween(1, 255), Fx.RandBetween(1, 255)))

End Sub

Sub ★学習データをセットする(R_取込 As Long)
    
    Dim R1st検索シート As Long: R1st検索シート = WS近似カラー検索.DataRangeカラー定数一覧.Row
    
    Dim R_出力 As Long
    R_出力 = GetRLast(WS近似カラー検索) + 1
    
    Intersect(WS近似カラー検索.DataRangeカラー定数一覧, WS近似カラー検索.Rows(R_取込)).Copy _
        WS近似カラー検索.Cells(R_出力, 1)
    WS近似カラー検索.Cells(R_出力, CNo近似カラー検索.RGBテキスト) _
        = WS近似カラー検索.Cells(R_出力, CNo近似カラー検索.RGBテキスト) & "ダミー"
    WS近似カラー検索.Cells(R_出力, CNo近似カラー検索.RGBテキスト).Resize(1, 4).Font.Color = RGB(255, 0, 0)
    
    WS近似カラー検索.Cells(R_出力, CNo近似カラー検索.R) = WS近似カラー検索.Cell対象カラーR
    WS近似カラー検索.Cells(R_出力, CNo近似カラー検索.G) = WS近似カラー検索.Cell対象カラーG
    WS近似カラー検索.Cells(R_出力, CNo近似カラー検索.B) = WS近似カラー検索.Cell対象カラーB
    
    WS近似カラー検索.DataRangeカラー定数一覧.Sort WS近似カラー検索.SortKey近似スコア
    
End Sub

標準モジュール:Ut_汎用関数

Option Explicit

' WorksheetFunctionの短縮取得
Function Fx() As WorksheetFunction
    Set Fx = WorksheetFunction
End Function

' 自動更新の停止
Sub エクセルの自動更新を停止する(ブック計算をOFF As Boolean _
    , Optional 画面更新をOFF As Boolean = True, Optional イベントをOFF As Boolean = True _
    , Optional カーソルをOFF As Boolean = True)
    With Application
        If ブック計算をOFF Then .Calculation = xlCalculationManual
        If 画面更新をOFF Then .ScreenUpdating = False
        If イベントをOFF Then .EnableEvents = False
        If カーソルをOFF Then .Cursor = xlWait
    End With
End Sub

' 自動更新の開始
Function エクセルの自動更新を開始する()
    With Application
        .Calculation = xlCalculationAutomatic
        .ScreenUpdating = True
        .EnableEvents = True
        .StatusBar = False
        .DisplayAlerts = True
        .Cursor = xlDefault
    End With
    ' 合わせて検索オプションを部分一致に設定
    Call Cells.Find("", lookat:=xlPart)
End Function

' 最終行の取得
Function GetRLast(指定オブジェクト As Variant, Optional ByVal C As Long = -1) As Long

    ' 渡されたオブジェクトからセル範囲を取得
    Dim 対象セル範囲 As Range
    Select Case TypeName(指定オブジェクト)
    Case "Range"
        Set 対象セル範囲 = 指定オブジェクト
    Case "Worksheet"
        Set 対象セル範囲 = 指定オブジェクト.UsedRange
    Case "AutoFilter", "ListObject"
        Set 対象セル範囲 = 指定オブジェクト.Range
    Case Else
        Err.Raise 1000, , "対象外のオブジェクト「" & TypeName(指定オブジェクト) & "」が指定されました。"
    End Select
    
    ' エリアの最終行を取得
    GetRLast = 対象セル範囲.Rows.Count + 対象セル範囲.Row - 1
    
    ' 列が指定されていればその列の入力最終行を取得
    If C <> -1 Then
        Do While 対象セル範囲.Worksheet.Cells(GetRLast, C) = ""
            GetRLast = GetRLast - 1
            If GetRLast < 対象セル範囲.Row Then
                GetRLast = 0
                Exit Function
            End If
        Loop
    End If
End Function

' 値のコピー
Sub 値をコピーする(ペースト基準セル As Range, コピー基準セルまたはエリア As Range _
    , Optional ブロック高 As Long = -1, Optional ブロック幅 As Long = -1)
    
    ' ◇ ブロックサイズ省略時はコピーエリアのサイズを取得
    If ブロック高 = -1 Then ブロック高 = コピー基準セルまたはエリア.Rows.Count
    If ブロック幅 = -1 Then ブロック幅 = コピー基準セルまたはエリア.Columns.Count
    
    ' 値をコピー
    ペースト基準セル.Resize(ブロック高, ブロック幅).Value = コピー基準セルまたはエリア.Resize(ブロック高, ブロック幅).Value

End Sub

コードの解説

① 近似色の検索

まずは近似色を探している部分ですが、こちらはストレートな処理で
近似スコアでソートして最小値=最近似の色を取得」しています。

RGB値を取得・計算するコードを簡単に書けるように、
RGB計算を補助するクラスを作ってコードを整理整頓してみました。


このクラスを使うと、

Option Explicit

Public clsRGBテスト As New ClassRGB操作 ' ← テスト用にPublicインスタンスを生成

Sub RGBクラステスト()
    
    clsRGBテスト.RGB = Selection.Interior.Color
    
    Debug.Print clsRGBテスト.R
    Debug.Print clsRGBテスト.G
    Debug.Print clsRGBテスト.B
    ' ↑ RGB値に代入するとR、G、Bそれぞれの値が裏で計算されている
    
    clsRGBテスト.R = 200
    Debug.Print clsRGBテスト.RGBテキスト
    ' ↑ Rを200にするなど変更も可能。RGB値が連動して変更される    

    Set clsRGBテスト.リンクするセル = WS近似カラー検索.Cell対象カラー
    ' ↑クラスをセルとリンクさせることもできる

    Debug.Print clsRGBテスト.RGBテキスト
    ' リンクしたセルの背景色が自動でセットされている

    clsRGBテスト.R = 200
    clsRGBテスト.G = 100
    clsRGBテスト.B = 150
    ' ↑ クラスのR、G、B値をいじると、セルの色が連動して変わる

End Sub

こんな風にRGB値を直感的にいじることができるようになります。


これを使うことで、RGB操作系のコードやChangeイベントの内容が
スッキリしたコードにできているところを見てみてください。


あとはシートモジュールの記載ですが、

Property Get Cell対象カラー() As Range
    Set Cell対象カラー = Range("B2")
End Property

こんな風にProperty Get プロシージャで固定セルを定義することで、
近似カラーシートのB2セルを以下のように書けるようにしています。

WS近似カラー検索.Cell最近似カラー

 
「シートオブジェクト名.定義したプロパティ名」で各セルを参照でき、
読むのも書くのも楽になっているのがわかると思います。

今回のように他ブックが登場しないマクロである場合はこの方法が活用できますので、
気に入ったら採用してみてください。

近似スコアの計算方法と学習

さて今回の課題の面白いポイントは「何をもって似た色とするか」です。

いろいろな方がいろいろな方法で色の近似を取っていますので、
是非とも皆さんの手法を眺めてみてください。


◇ 「最も近い色を見つけよう!」への回答集はこちら
Discord:☆回答【VBA】#002 最も近い色を見つけよう!

※上記はDiscord参加済みの方用のURLです
 未参加の方は下記からご参加ください!
www.limecode.jp


さて私の方法はというと、
とりあえずはRGBを3次元と見立てた距離でやってみました。

√(R - R')² + (G - G')² + (B - B')²、いわゆるユークリッド距離ですね。


しかしこれやってみるとわかるんですが、
全然違う色を持ってくることが多々あるんですよね(´∀`;)
間違った近似

RGB値自体が正確には3次元空間ではない上に、
例えば「R、G、Bのうち最も高い値が明るさ(明度)になっている」など、
色ならではの特性もあったようです。


そこで取った策として、

  • 明らかにもっと近い色がある場合は現在の色をそのカラー定数の最近似色として記録(学習)させる
  • しかしその色はカラー定数ではないので、その色が選ばれた場合は対応するカラー定数が選ばれたものとする

という、ダミーカラー定数の学習という形で対応しました。
 
ダミーカラー定数
実際は(252,20,71)なんだけど登録上は(255,0,0)のカラーを学習させることで、
今後付近の色をこの色に近似してくれるようになります。

※ このアイデアはさくたん様より教えていただきました。
具体的な手法を画像付きで解説してくださっていますので、
興味がある方は回答スレッドを見に行ってみてください。

 
流行りの機械学習ではなく、機械的に人間が手動で学習させた力業ですが、
割と近似精度を高めることが出来たんじゃないかと思います。

おまけ:カラー定数偏りすぎ問題

この課題をやっていて思ったのですが、カラー定数偏りすぎなんですよ笑


こんな風に全然似た色がない領域もありますし↓
カラー定数の偏り


かと思えばこんな風に選びたい放題な領域もあります↓


定数設定された方、アイボリーというかクリーム色というか、
そのあたりの薄い黄色好きすぎじゃないですかね?笑

レモンシフォン、パパイヤホイップ、ブランシュアーモンド
とかそんなに薄い黄色をたくさんいただいても使いこなせないんですが。。。笑


まあ別に何かに困るということはないんですが、
なんとなく気になってしまったのでこちらに言及して本記事を締めました(´∀`;)