Excel VBA 速度改善

仕事でExcelVBAを触る機会があったので、その時の改善内容をメモ。
キー項目が複数個あるデータをループでチェックする際に処理速度が非常に遅いマクロがあった。

チェック対象
キー項目1 キー項目2 チェック結果
0001 A001 100,000
0002 A003 10,000

チェックリスト
キー項目1 キー項目2 値範囲開始 値範囲終了
0001 A001 10,000 500,000
0002 A003 100,000 500,000

チェック対象を1行読んで、その値でチェックリストを全件ループし、キー項目1とキー項目2が一致していればその値の範囲をチェックするような仕組みだった。
例)
 For i = 1 To  チェック対象MAX行
      For j = 1 To チェックリストMAX行
   If キー項目1(チェック対象) = キー項目1(チェックリスト) And キー項目2(チェック対象) = キー項目2(チェックリスト) Then

                'チェック処理
 
         End If
    Next j
 Next i


チェック対象とチェックリストが少なければ大した問題にはならなかったが、データ件数が増えてくると1回の処理が30分かかったりするようになってしまった。まぁ当然だけども・・・。
とはいえ、ある程度稼働した後だったのであんまり手を入れるのが怖かったので下記の関数を作った。っていうか探してきた。

'---------------------------------------------------------
' 文字列・数字セルを検索
' シート名と列位置を指定し検索する。該当した行位置をカンマ区切りで返す。
'  1件ずつループしながらIf関数で1行ずつ判定するより圧倒的に高速
'  複数条件は対応外なので、この関数で該当行を絞ったうえで、If関数で詳細なチェックをかけたほうが高速になる
' 利用例)GetSearchStringNumberCell("0001","A:A","データ")
' 戻り値例)4,15,20
'---------------------------------------------------------
Public Function GetSearchStringNumberCell(ByVal strValue As String, ByVal strColumnRange As String, ByVal strSheetName As String) As String

    Dim rng As Range
    Dim adr As String
    Dim strResult As String

    GetSearchStringNumberCell = ""
  
    Set rng = Sheets(strSheetName).Columns(strColumnRange).Find(strValue)
  
    If rng Is Nothing Then
        Exit Function
    Else
        adr = rng.Address
        strResult = rng.Row
    End If

    Do
    Set rng = Sheets(strSheetName).Columns(strColumnRange).FindNext(After:=rng)
    If rng.Address = adr Then
            Exit Do
        Else
            strResult = strResult & "," & rng.Row
        End If
    Loop

    GetSearchStringNumberCell = strResult

End Function

Excelの標準機能を使うと処理速度が速くなるのは有名な話なので、検索する処理をExcelのFind関数に任せてみた。
Find関数は1項目しか検索できないので、1回で処理しようとすると複数項目を検索しようとすると検索用のキー項目を作る必要がある。
例)キー項目1+キー項目2の値を横の列に作成して検索。(0001A002)

でも、すでに稼働しているところに手を入れるのは怖いので、この関数をここに入れてみた。

例)

 For i = 1 To  チェック対象MAX行
      Dim strValue As String
      Dim strArr() As String
  strValue = GetSearchStringNumberCell(キー項目1(チェック対象),"A:A","チェック対象
")
      strArr = Split(strValue,",")

      For jj = 0 To Ubound(strArr)
         j = strArr(jj)

   If キー項目1(チェック対象) = キー項目1(チェックリスト) And キー項目2(チェック対象) = キー項目2(チェックリスト) Then

                'チェック処理
 
         End If
    Next j
 Next i

GetSearchStringNumberCell関数は検索値、検索範囲(列など)、検索シートを指定し、実行することで検索値が含まれているセルの行位置をカンマ区切りで返してくれる。
そのため、戻り値をカンマでSplitすることで、チェックする必要のある行位置のみをループすればよいことになる。

この処理のおかげで処理速度が30分が30秒以下で終わるようになった。やっぱりExcel関数をなるべく使うほうがいいんだけど、使い方に慣れた関数でないとループやIf関数で処理をしたくなる。

あとは2次元配列にしたりすれば貼り付けの速度もかなり早くなるんだろうけど、そこまで改修しなくても問題ない速度になったのでここまでにしておこうと思う。
業務上支障のない速度になっていれば問題ないし、変に手を入れて、30秒が10秒になる程度であればコーヒーでも飲んでてもらえればいいし。
※改修とテストにそれ以上時間かかっちゃうので優先順位が低いってのは内緒

ちなみに日付セルを検索する場合はこっち
'---------------------------------------------------------
' 日付セルを検索
' シート名と列位置を指定し検索する。該当した行位置をカンマ区切りで返す。
'  1件ずつループしながらIf関数で1行ずつ判定するより圧倒的に高速
'  複数条件は対応外なので、この関数で該当行を絞ったうえで、If関数で詳細なチェックをかけたほうが高速になる
' 利用例)GetSearchDateCell("2020/08/01","A:A","データ")
' 戻り値例)4,15,20
'---------------------------------------------------------

Public Function GetSearchDateCell(ByVal strValue As Date, ByVal strColumnRange As String, ByVal strSheetName As String) As String

    Dim rng As Range
    Dim adr As String
    Dim strResult As String

    GetSearchDateCell = ""
  
  Set rng = Sheets(strSheetName).Columns(strColumnRange).Find(strValue)
  
  If rng Is Nothing Then
        Exit Function
    Else
        adr = rng.Address
        strResult = rng.Row
    End If

    Do
    Set rng = Sheets(strSheetName).Columns(strColumnRange).FindNext(After:=rng)
    If rng.Address = adr Then
            Exit Do
        Else
            strResult = strResult & "," & rng.Row
        End If
    Loop

    GetSearchDateCell = strResult

End Function

引数が変わっただけ・・・。Object型とかVariant型にすれば統一できそうな気もする。

WPFのスタイルの継承について

WPFでアプリを作る際にスタイルの継承をしてみたかったので調べてみた。

「BasedOn」を指定することでスタイルの継承を行うことができる。
そのため、「基本スタイルの定義」->(継承)「コントロール共通のスタイルの定義」->(継承)「プロジェクトごとのスタイルの定義」とすることで、業務用アプリなどの同じ意味を持つコントロールの定義を一括で定義することができる。
業務用アプリだと「得意先CD」「得意先名」など同じ意味を持つテキストボックスコントロールが各画面にばらまかれるため、MaxLengthや色などを変更するときに作り方次第では画面ごとに修正が必要だが、共通スタイルを定義しておくことでResourceを修正するだけで全画面が直る。

ResourceDictionary.MergedDictionariesで継承元の定義しておいたXAMLを読み込んでいる。
最後にApp.xamlでも同じように定義を読み込む

サンプル)
[BasicResource.xaml]
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:SystemTemplate.resource"
                    >
    
    <!-- Active状態の背景色 -->
    <SolidColorBrush x:Key="ActiveBackColor" Color="#FFF4FDBE"></SolidColorBrush>

    <!-- テーマカラー -->
    <SolidColorBrush x:Key="WindowColor" Color="#FFF4F9FF"></SolidColorBrush>

    <!--#region 継承元のスタイル-->
    <!-- 継承元のフォームベースのスタイル -->
    <Style x:Key="WindowStyleBase" TargetType="Window">
        <!--<Setter Property="Icon" Value="images/icon.ico"></Setter>-->
        <Setter Property="FontFamily" Value="MS Gothic" />
        <Setter Property="FontSize" Value="13"/>
        <Setter Property="Background" Value="{StaticResource WindowColor}"></Setter>
        <Setter Property="WindowStyle" Value="SingleBorderWindow"></Setter>
        <Setter Property="TextOptions.TextFormattingMode" Value="Ideal"></Setter>
        <Setter Property="TextOptions.TextRenderingMode" Value="Auto"></Setter>
    </Style>


    <Style TargetType="UserControl" x:Key="UserControlBaseStyle">
        <Setter Property="FontSize" Value="11.5"/>
        <Setter Property="FontFamily" Value="MS Gothic"/>
    </Style>

    <Style x:Key="TextBlockStyleBase" TargetType="TextBlock">
        <Setter Property="FontFamily" Value="MS Gothic" />
        <Setter Property="FontSize" Value="13"/>
        <Setter Property="TextAlignment" Value="Left"/>
        <Setter Property="VerticalAlignment" Value="Center"/>
    </Style>

    <Style x:Key="TextBoxStyleBase" TargetType="TextBox">
        <Setter Property="FontFamily" Value="MS Gothic" />
        <Setter Property="FontSize" Value="13"/>
        <Setter Property="TextAlignment" Value="Left"/>
        <Setter Property="VerticalAlignment" Value="Center"/>
        <Setter Property="VerticalContentAlignment" Value="Center"/>
        <Setter Property="Height" Value="25"/>

        <!-- フォーカス時に背景色を設定 -->
        <Style.Triggers>
            <Trigger Property="IsFocused" Value="True">
                <!--IsFocusedがTrueの場合、下の値を適用-->
                <Setter Property="Background" Value="{StaticResource ActiveBackColor}" />
            </Trigger>
            <Trigger Property="IsFocused" Value="False">
                <Setter Property="Background" Value="White" />
            </Trigger>
        </Style.Triggers>

    </Style>


    <Style x:Key="PasswordBoxStyleBase" TargetType="PasswordBox">
        <Setter Property="FontFamily" Value="MS Gothic" />
        <Setter Property="FontSize" Value="13"/>
        <Setter Property="VerticalContentAlignment" Value="Center"/>
        <Setter Property="Height" Value="25"/>

        <!-- フォーカス時に背景色を設定 -->
        <Style.Triggers>
            <Trigger Property="IsFocused" Value="True">
                <!--IsFocusedがTrueの場合、下の値を適用-->
                <Setter Property="Background" Value="{StaticResource ActiveBackColor}" />
            </Trigger>
            <Trigger Property="IsFocused" Value="False">
                <Setter Property="Background" Value="White" />
            </Trigger>
        </Style.Triggers>

    </Style>

    <Style x:Key="LabelStyleBaseNoHeight" TargetType="Label">
        <Setter Property="FontFamily" Value="MS Gothic" />
        <Setter Property="FontSize" Value="13"/>
        <Setter Property="BorderBrush" Value="Black"/>
        <Setter Property="BorderThickness" Value="1"/>
    </Style>

    <Style x:Key="LabelStyleBase" TargetType="Label" BasedOn="{StaticResource LabelStyleBaseNoHeight}">
        <Setter Property="Height" Value="25"/>
        <Setter Property="VerticalAlignment" Value="Center"/>
    </Style>

    <Style x:Key="DataGridStyleBase" TargetType="DataGrid">
        <Setter Property="FontFamily" Value="MS Gothic" />
        <Setter Property="FontSize" Value="13"/>
        <Setter Property="HorizontalGridLinesBrush" Value="Silver"/>
        <Setter Property="VerticalGridLinesBrush" Value="Silver"/>
        <Setter Property="AlternatingRowBackground" Value="#FFD8FFFD"/>
        <!--<Setter Property="Height" Value="25"/>-->
    </Style>

    <!-- 影付きのボタンにするためのエフェクト -->
    <DropShadowEffect x:Key="ButtonEffect" BlurRadius="0" RenderingBias="Quality" ShadowDepth="2"/>
    <Style x:Key="ButtonStyleBase" TargetType="Button">
        <Setter Property="FontFamily" Value="MS Gothic" />
        <Setter Property="FontSize" Value="13"/>
        <Setter Property="BorderBrush" Value="Black"/>
        <Setter Property="BorderThickness" Value="1"/>
        <Setter Property="Height" Value="25"/>
        <Setter Property="Effect" Value="{StaticResource ButtonEffect}"/>

        <Style.Triggers>

            <!-- フォーカス時の背景色 -->
            <Trigger Property="IsFocused" Value="True">
                <Setter Property="Background" Value="{StaticResource ActiveBackColor}"></Setter>
            </Trigger>
        </Style.Triggers>

    </Style>

    <!-- TextBlock縦書 -->
    <Style x:Key="TextBlockVerticalWriting"  TargetType="{x:Type TextBlock}">
        <Setter Property="RenderTransformOrigin" Value="0.5,0.5"/>
        <Setter Property="Margin" Value="0.0,0.0"/>
        <Setter Property="Width" Value="Auto"/>
        <Setter Property="TextOptions.TextFormattingMode" Value="Display"/>
        <Setter Property="RenderTransform">
            <Setter.Value>
                <TransformGroup>
                    <ScaleTransform ScaleX="1" ScaleY="1"/>
                    <SkewTransform AngleX="0" AngleY="0"/>
                    <RotateTransform Angle="0"/>
                    <TranslateTransform X="0" Y="0"/>
                </TransformGroup>
            </Setter.Value>
        </Setter>
        <Setter Property="LayoutTransform">
            <Setter.Value>
                <TransformGroup>
                    <ScaleTransform ScaleX="1" ScaleY="1"/>
                    <SkewTransform AngleX="0" AngleY="0"/>
                    <RotateTransform Angle="90"/>
                    <TranslateTransform X="0" Y="0"/>
                </TransformGroup>
            </Setter.Value>
        </Setter>
    </Style>

    <!--#endregion-->
</ResourceDictionary>

[ControlResource] コントロールごとの共通スタイルを定義
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:SystemTemplate.resource"
                    >

    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="BaseResorce.xaml"/>
    </ResourceDictionary.MergedDictionaries>

    <!--#region 継承後のスタイル-->

    <Style x:Key="NormalLabel" TargetType="Label" BasedOn="{StaticResource LabelStyleBase}">
        <Setter Property="Background" Value="#FFCEE4FF" />
    </Style>
    <Style x:Key="NormalLabelNoHeight" TargetType="Label" BasedOn="{StaticResource LabelStyleBaseNoHeight}">
        <Setter Property="Background" Value="#FFCEE4FF" />
    </Style>

    <Style TargetType="UserControl" x:Key="CsDate" BasedOn="{StaticResource UserControlBaseStyle}">
    </Style>

    <Style x:Key="TitleLabel" TargetType="Label" BasedOn="{StaticResource LabelStyleBase}">
        <Setter Property="Background" Value="#FF7BB7FF" />
    </Style>

    <Style x:Key="RequiredLabel" TargetType="Label" BasedOn="{StaticResource LabelStyleBase}">
        <!--<Setter Property="Background" Value="#FFFF997B" />-->
        <Setter Property="Background" Value="#FF7BB7FF" />
    </Style>

    <Style x:Key="SimpleLabel" TargetType="Label" BasedOn="{StaticResource LabelStyleBase}">
        <Setter Property="BorderThickness" Value="0"/>
    </Style>

    <Style x:Key="NormalTextBlock" TargetType="TextBlock" BasedOn="{StaticResource TextBlockStyleBase}">
    </Style>

    <Style x:Key="NormalPasswordBox" TargetType="PasswordBox" BasedOn="{StaticResource PasswordBoxStyleBase}">
    </Style>


    <!--#region DataGrid関連 -->

    <!-- ヘッダセルに対する書式設定
                 ・ヘッダ部は水平中央寄せ
            -->
    <Style x:Key="DataGridHeaderCellCenter" TargetType="DataGridColumnHeader">
        <Setter Property="HorizontalContentAlignment"  Value="Center"/>
    </Style>

    <!-- 各セルに対する書式設定
                 ・セルの垂直中央寄せ
            -->
    <Style x:Key="DataGridCellCenter" TargetType="DataGridCell" >
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type DataGridCell}">
                    <Grid Background="{TemplateBinding Background}">
                        <ContentPresenter VerticalAlignment="Center" />
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>

    </Style>

    <Style x:Key="NormalDataGrid" TargetType="DataGrid" BasedOn="{StaticResource DataGridStyleBase}">
        <Setter Property="RowHeight" Value="20"/>
        <Setter Property="ColumnHeaderStyle" Value="{StaticResource DataGridHeaderCellCenter}" />
        <Setter Property="CellStyle" Value="{StaticResource DataGridCellCenter}" />
    </Style>


    <Style x:Key="DataGridAutoRowHeight" TargetType="DataGrid" BasedOn="{StaticResource DataGridStyleBase}">
        <Setter Property="ColumnHeaderStyle" Value="{StaticResource DataGridHeaderCellCenter}" />
        <Setter Property="CellStyle" Value="{StaticResource DataGridCellCenter}" />
    </Style>

    <Style x:Key="DataGridCellElementStyleCenter" TargetType="{x:Type TextBlock}">
        <Setter Property="TextAlignment" Value="Center" />
    </Style>

    <Style x:Key="DataGridCellElementStyleLeft" TargetType="{x:Type TextBlock}">
        <Setter Property="TextAlignment" Value="Left" />
    </Style>

    <Style x:Key="DataGridCellElementStyleRight" TargetType="{x:Type TextBlock}">
        <Setter Property="TextAlignment" Value="Right" />
        <Setter Property="Margin" Value="0,0,5,0" />
    </Style>

    <!--#endregion DataGrid関連 -->

    <Style x:Key="SystemTitleTextBlock" TargetType="TextBlock" BasedOn="{StaticResource TextBlockStyleBase}">
        <Setter Property="FontFamily" Value="MS Gothic"/>
        <Setter Property="FontSize" Value="24"/>
        <Setter Property="FontWeight" Value="Bold"/>
        <Setter Property="VerticalAlignment" Value="Center"/>
    </Style>


    <Style x:Key="TitleTextBlock" TargetType="TextBlock" BasedOn="{StaticResource TextBlockStyleBase}">
        <Setter Property="FontFamily" Value="MS Gothic"/>
        <Setter Property="FontSize" Value="13"/>
        <Setter Property="FontWeight" Value="Bold"/>
        <Setter Property="VerticalAlignment" Value="Center"/>
    </Style>

    <Style x:Key="SupplementTextBlock" TargetType="TextBlock" BasedOn="{StaticResource TextBlockStyleBase}">
        <Setter Property="FontSize" Value="10"/>
        <Setter Property="Foreground" Value="#FF616161"/>
    </Style>

    <!--▽ 通常のテキストボックスのスタイル -->
    <Style x:Key="NormalTextBox" TargetType="TextBox" BasedOn="{StaticResource TextBoxStyleBase}">
    </Style>

    <!--▽ 読み取り専用のテキストボックスのスタイル -->
    <Style x:Key="ReadOnlyTextBox" TargetType="TextBox" BasedOn="{StaticResource TextBoxStyleBase}">
        <Setter Property="IsReadOnly" Value="True"/>
        <Setter Property="Background" Value="WhiteSmoke"/>
    </Style>

    <!--▽ 複数行のテキストボックスのスタイル -->
    <Style x:Key="MultiLineTextBox" TargetType="TextBox" BasedOn="{StaticResource TextBoxStyleBase}">
        <Setter Property="AcceptsReturn" Value="True"/>
        <Setter Property="VerticalScrollBarVisibility" Value="Auto"/>
        <Setter Property="HorizontalScrollBarVisibility" Value="Auto"/>
        <Setter Property="VerticalAlignment" Value="Top"/>
        <Setter Property="VerticalContentAlignment" Value="Top"/>
        <Setter Property="TextWrapping" Value="Wrap"/>
        <Setter Property="Padding" Value="2"/>
    </Style>

    <Style x:Key="NormalButton" TargetType="Button" BasedOn="{StaticResource ButtonStyleBase}">
    </Style>

    <Style x:Key="MenuButton" TargetType="Button" BasedOn="{StaticResource ButtonStyleBase}">
        <Setter Property="HorizontalContentAlignment" Value="Left"/>
        <Setter Property="Margin" Value="10"/>
    </Style>
    
    <Style x:Key="WindowStyle" TargetType="Window" BasedOn="{StaticResource WindowStyleBase}">
    </Style>

    <Style x:Key="SignInWindowStyle" TargetType="Window" BasedOn="{StaticResource WindowStyleBase}">
        <!--<Setter Property="Background">
                    <Setter.Value>
                        <LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
                            <LinearGradientBrush.GradientStops>
                                <GradientStop Offset="0.0" Color="#FFB0FFB0"/>
                                
                <GradientStop Offset="1.0" Color="#FFD6D6D6"/>
                
                                <GradientStop Offset="1.0" Color="#FFB6DAA2"/>
                            </LinearGradientBrush.GradientStops>
                        </LinearGradientBrush>
                    </Setter.Value>
            </Setter>-->
    </Style>


    <Style x:Key="GridSplitterStyle" TargetType="GridSplitter">
        <Setter Property="Background" Value="LightGray"></Setter>
        <Setter Property="ShowsPreview" Value="True"></Setter>
        <Setter Property="IsTabStop" Value="False"></Setter>
    </Style>


</ResourceDictionary>

[ProjectResource] プロジェクト(案件)ごとのスタイルを定義
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:SystemTemplate.resource"
                    xmlns:convreter="clr-namespace:SystemTemplate.resource.converter"
                    
                    >
    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="ControlResource.xaml"/>
    </ResourceDictionary.MergedDictionaries>

    <convreter:StringToNumberConverter x:Key="stringToNumberConverter" />

    <!--#region Window -->
    <Style x:Key="LoginWindowStyle" TargetType="Window" BasedOn="{StaticResource WindowStyle}">
    </Style>

    <Style x:Key="MenuWindowStyle" TargetType="Window" BasedOn="{StaticResource WindowStyle}">
        <Setter Property="FontSize" Value="16"/>
        <Setter Property="Height" Value="740"/>
        <Setter Property="Width" Value="980"/>
    </Style>

    <Style x:Key="ShnWindowStyle" TargetType="Window" BasedOn="{StaticResource WindowStyle}">
        <Setter Property="Height" Value="740"/>
        <Setter Property="Width" Value="980"/>
    </Style>

    <!--#region ユーザーマスタ -->
    <Style x:Key="txtUserCDStyle" TargetType="TextBox" BasedOn="{StaticResource  NormalTextBox}">
        <Setter Property="MaxLength" Value="4"/>
        
        <!--数値入力のみとする-->
        <Setter Property="InputMethod.IsInputMethodEnabled" Value="False"/>
    </Style>

    <Style x:Key="txtPassWord" TargetType="TextBox" BasedOn="{StaticResource  NormalTextBox}">
        <Setter Property="MaxLength" Value="10"/>
    </Style>
     <!--#endregion-->

    <Style x:Key="txtTnkStyle" TargetType="TextBox" BasedOn="{StaticResource  NormalTextBox}">
        <Setter Property="MaxLength" Value="9"/>

        <Setter Property="InputMethod.IsInputMethodEnabled" Value="False"/>
        
        <!--カンマ区切りで小数点以下なし表示-->
        <Setter Property="Text" Value="{Binding Path=., Converter={StaticResource stringToNumberConverter}}" />

    </Style>

</ResourceDictionary>

[App.xaml]
<Application x:Class="SystemTemplate.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:SystemTemplate"
             StartupUri="win00_0000_Login.xaml">
    <Application.Resources>
        <ResourceDictionary>

            
            <!-- Static Resourceが設定されていないコントロールを判別するためのスタイル。個別でStaticResouceを設定していればそちらが上書きされる -->
            <Style TargetType="TextBox">
                <Setter Property="Background" Value="Red"/>
            </Style>

            <!--#region プロジェクトごとのコントロールを定義したリソース -->
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="resource\ProjectResource.xaml"/>
            </ResourceDictionary.MergedDictionaries>
            <!--#endregion-->
            
        </ResourceDictionary>


    </Application.Resources>
</Application>

WPFの画面表示速度について

TreeListViewの画面表示が遅かったのでいろいろ調べてみた。
結論的には、WPFが遅いんじゃなくて、私の使い方が間違っているだけだった。
WPFはいろんなことができるけど、使い方を間違っちゃうことが多いのでまだまだ場数が足りないのかなと思う。

250アイテムを表示するのに20秒以上かかっている。

どうやら画面表示が遅いので、いろいろと試してみた結果、TreeListViewにTextBoxを利用しているのが原因みたい。

TextBlockに置き換えることで比べ物にならないぐらい早くなった。
確かにTextBoxを250行×5列の数だけ表示していれば遅くなるのも当然な気がする。

常に編集状態にする必要はないのでExcelみたいに編集モードになった箇所だけTextBoxを表示して編集できるように修正をした。

Bool値をVisiblityに変換するConverterを定義し、普段はTextBlockを表示しておいて、IsSelectedプロパティがTrueになったときだけ、TextBoxのVisibilityをCollapsedからVisibleに変換する。

<GridViewColumn x:Name="gvcTitle" Header="タイトル" Width="200" HeaderContainerStyle="{StaticResource GridViewColumnHeaderStyle}">
    <GridViewColumn.CellTemplate>
        <DataTemplate>
            <!-- Marginで左右から-6pxとしないとHearder部のタイトルとラインの位置がずれる.GridViewの既知の不具合 -->
            <Border BorderBrush="Black"  BorderThickness="0,0,0.5,0" Margin="-6,0,-6,0">
                <DockPanel>
                    <!--The Expander Button (can be used in any column (typically the first one))-->
                    <TreeListView:TreeListViewExpander Focusable="False" Visibility="{Binding TitleExpanderVisibility}"/>
                    <TreeListViewSubControls:TreeListItemTextBox Text="{Binding Data.Title, Mode=TwoWay}" VerticalContentAlignment="Center" VerticalAlignment="Stretch" Margin="2,2,2,2" HorizontalContentAlignment="Stretch"

                                                                 Style="{StaticResource EditTreeListTextBoxStyle}"  HorizontalAlignment="Stretch" BorderThickness="0" Opacity="1"  TabIndex="1" 

                                                                 Width="{Binding Path=ActualWidth, RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type DockPanel}}}" IsReadOnly="{Binding IsReadOnly}"

                                                                 Visibility="{Binding IsSelected,Converter={StaticResource boolToVisiblityConverter},ConverterParameter=True}"/>
                    <Border BorderThickness="0" Visibility="{Binding IsSelected,Converter={StaticResource boolToVisiblityConverter},ConverterParameter=False}" Margin="2,2,2,2" HorizontalAlignment="Stretch">
                        <TreeListViewSubControls:TreeListItemTextBlock Text="{Binding Data.Title, Mode=TwoWay}"  Style="{StaticResource TreeListTextBlockStyle}"  Width="{Binding Path=ActualWidth, RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type DockPanel}}}"  />
                    </Border>
                </DockPanel>
            </Border>
        </DataTemplate>
    </GridViewColumn.CellTemplate>
</GridViewColumn>

WPFのコンボボックスの背景色について

TreeListView上でコンボボックスを利用するときにフォーカスのあるコンボボックスの色をイベントで変えようとしたけど上手くいかなかった。

色々と調べているとXaml側のテンプレートをいじる必要があるみたいだったので、いろいろとやってみた。

とりあえず、コンボボックスをデザイナに配置し、コンボボックスを選択した状態で「右クリック」->「テンプレートの編集」ー>「コピーして編集」を選択する。


リソースの名前は後でも変更できるのと、自動で作成されるXAMLの場所も移動できるので一旦は初期のままにして「OK」を押す。


ものすごく長いリソースがWindow.Resourceの中に作成される


このリソースの中でBackgroundを管理している箇所のStyleを変更すれば変えることができる。具体的には下記の画像の部分。
※編集可能なコンボボックスを使っているときは「ComboBox.Static.Editable.Background」を変更すればよいと思われる。




今回の場合は、フォーカスの移動によって背景色を変更したかったので、「ComboBox.Static.Background」、「ComboBox.MouseOver.Background」をデザイナ上は「Transparent」に設定しておいて、プログラムからGotFocusイベントとLostFocusイベントを利用して変更している。

=== ソース抜粋1 ===
            // 選択中の背景色を設定する
            this.GotFocus += SetSelectionBackgroundColor;
            // 選択中の背景色を初期値にもどす
            this.LostFocus += ClearSelectionBackgroundColor;
==============

=== ソース抜粋2 ===

        /// <summary>
        /// フォーカス取得時の背景色
        /// </summary>
        public Brush SelectionBackgroundColor = new SolidColorBrush(Color.FromArgb(255, 250, 250, 0));

        /// <summary>
        /// 選択中のコンボボックスの背景色を設定
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void SetSelectionBackgroundColor(object sender, RoutedEventArgs e)
        {
            var grid = this.Template.FindName("templateRoot", this) as Grid;
            if (grid != null)
                grid.Background = SelectionBackgroundColor;
        }

        /// <summary>
        /// 選択中のコンボボックスの背景色を初期化
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ClearSelectionBackgroundColor(object sender, RoutedEventArgs e)
        {
            var grid = this.Template.FindName("templateRoot", this) as Grid;
            if (grid != null)
                grid.Background = null;
        }

=============
※templateRootは先ほどデザイナから自動生成されたXAMLの中で「ComboBoxTemplate」というControlTemplateがあるが、その中のGridのこと。
 なのでコンボボックスを構成する各コントロールを配置している元のGridの背景色を変更しているだけ。逆に言うとコンボボックスをどうやって作っているのかがControlTemplateを見ればわかる。


後はこの作成したリソースをコンボボックスのStyleに適用すればよい。

=== ソース抜粋3 ===
<ComboBox  ItemsSource="{Binding PriorityList, Mode=TwoWay}"
DisplayMemberPath="Name" SelectedValuePath="PriorityNo"
SelectedValue="{Binding SelectedPriorityNo}" Style="{StaticResource ComboBoxStyle1}">

=============

Window.Resourceに配置すると他のWindowでは利用できなくなるので、App.Xamlに配置すれば他のWindowから利用できる。
App.Xamlが長くなりすぎるのであれば、別のリソースディクショナリに作成して、App.Xamlは「ResourceDictionary.MergedDictionaries」でリソースの指定をすればよい。

<適用した例>


TreeListViewでのコンボボックスのデータバインドについて

TreeListViewにコンボボックスを表示するのに手間取ったのでメモ。

相変わらずゆっくりとガントチャートのアプリをいじっている。
前作っていたのはZipファイルにデータを保存する形式だったので、今度はSqliteにデータを保存する形に作り直している。

別のアプリのTaskManagerと共通のデータベースを利用すれば面白いのではないかと思って作っていたら、コンボボックスの表示がうまくできなかったので試行錯誤をしていた。



クラスとしてはこんな構造を持っていた。

メイン画面のDataContextにMainWindowViewModelをバインドして、TreeListViewには下記のような形でTreeViewへのバインドと同じような形で処理をしていた。

==== ソース抜粋 ====

               <TreeListView:TreeListView x:Name="treeView" Background="White" AllowsColumnReorder="True" ItemsSource="{Binding TreeListViewModel.TreeListItems ,Mode=TwoWay}">
                    <TreeListView:TreeListView.ItemTemplate>
                        <HierarchicalDataTemplate ItemsSource="{Binding Children}" />
                    </TreeListView:TreeListView.ItemTemplate>

===============

コンボボックスの中身をバインドでやろうとして、プロパティをMainViewModelやTreeListViewModelに実装し、「ItemsSource="{Binding MainWindowViewModel.ComboboxItems,Mode=TwoWay}">」や「ItemsSource="{Binding TreeListViewModel.ComboboxItems,Mode=TwoWay}">」と指定していたが、うまく表示されない。コンボボックスを開いてみても項目の生成すらされていなかったので、バインディング自体が上手くいっていなかったので、よくよく考えるとChildにはTreeListItemをバインドしているのだから、TreeListItemクラスにコンボボックスの中身を持つ必要があるんド絵はないかと思って、TreeListItemクラスにコンボボックスの内容を作成し、バインドしてみたら上手くいった・・・。

とはいえ、TreeListItemの中身にコンボボックスの要素を実体として持つなどあほらしすぎる。
というわけで、コンボボックスの実体はTreeListViewModelに作成しておいて、参照のみTreeListItemに持つようにした。


==== ソース抜粋1 ====

   /// <summary>
    /// TreeListViewにバインディングするクラス
    /// </summary>
    public class TreeListItem : Model.ModelBase
    {
        public TreeListViewViewModel TreeListVM;
        private ObservableCollection<Model.Status> _statusList;
        /// <summary>
        /// 状況コンボボックスに表示するリストを定義
        /// データのもとはTreeListViewModelに設定をし、TreeListItemは参照(Class)のみとする。
        /// </summary>
        public ObservableCollection<Model.Status> StatusList { get { return this._statusList; } set { this._statusList = value; NotifyPropertyChanged(); } }

===============

==== ソース抜粋1 ====

    /// <summary>
        /// TreeListItemを生成したときの初期設定
        /// </summary>
        /// <param name="task"></param>
        /// <param name="treeListViewViewModel"></param>
        public void Init(Model.Task task, TreeListViewViewModel treeListViewViewModel,TreeListItem parentTree)
        {
            this.Data = task;
            this.TreeListVM = treeListViewViewModel;

            // コンボボックス用の参照をセットする
            this.StatusList = treeListViewViewModel.StatusList;
            this.DifficultyList= treeListViewViewModel.DifficultyList;
            this.PriorityList = treeListViewViewModel.PriorityList;
            this.SelectedStatsuNo = Data.CardStatusNo;
            this.SelectedDifficultyNo = Data.DifficultyNo;
            this.SelectedPriorityNo= Data.PriorityNo;


            this.Data.TreeItem = this;

            if (parentTree != null)
            {
                SetParent(parentTree, null);
            }
        }

===============


==== ソース抜粋3 ====
                        <!-- *** 状態 *** -->
                        <GridViewColumn Header="状態" Width="80"  HeaderContainerStyle="{StaticResource GridViewColumnHeaderStyle}">
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <!-- Marginで左右から-6pxとしないとHearder部のタイトルとラインの位置がずれる.GridViewの既知の不具合 -->
                                    <Border BorderBrush="Black"  BorderThickness="0,0,0.5,0" Margin="-6,0,-6,0">
                                        <DockPanel   >
                                            <ComboBox  VerticalAlignment="Stretch" VerticalContentAlignment="Center" ItemsSource="{Binding StatusList,Mode=TwoWay}"
                                                                                           HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
                                                                                           DisplayMemberPath="Name" SelectedValuePath="StatusNo"
                                                                                           SelectedValue="{Binding SelectedStatsuNo}">
                                                <ComboBox.ItemTemplate>
                                                    <DataTemplate>
                                                        <TextBlock><Run Text="{Binding StatusNo, Mode=TwoWay}" /><Run Text=":" /><Run Text="{Binding Name, Mode=TwoWay}" /></TextBlock>
                                                    </DataTemplate>
                                                </ComboBox.ItemTemplate>
                                            </ComboBox>
                                        </DockPanel>
                                    </Border>
                                </DataTemplate>
                            </GridViewColumn.CellTemplate>
                        </GridViewColumn>
===============

クラスは参照型なので、TreeListViewModel側でインスタンス化し、各選択肢をリストに追加して、TreeListItemでは参照の設定のみとしておく。

昔はClassが参照型、Structureが値型だとかは知識としては知っていたけど、あまり意識して利用するところはなかった。(それはそれでどうなのかと思うけど)
こういう時にちゃんと知っていると便利だと思う。

ComboboxのBindingについては、他のいろんなサイト見れば参考になると思う。
そもそもTreeListViewでComboboxを使うような状況になっている人がどれだけいるのか謎だけど。









Webサイトに自動でログインするVBS

サイトに自動でログインするVBSのソース

事前に対象のWebサイトを開いて、F12キーを押下して、要素の名前を調べておく。
※Edge、Chromeなどのブラウザ

objIE.Visible = Trueの部分を
objIE.Visible = Falseにしておけば、ブラウザも表示されない。

========= 以下のソース ========

OpenUrl("http://XXXXXXXX.html","uid","user1","pass","xyz)

'URLを開く
Sub OpenUrl(URL,idElement,uid,passElement,pass,loginElement)
Dim objIE
    'IE起動
    Set objIE = CreateObject("InternetExplorer.Application")
    objIE.Visible = True

    'サイトに接続
    objIE.navigate URL

    'IEを待機
    Call IEWait(objIE)

    '5秒停止
    Call WaitFor(2)

    'ユーザーIDとパスワードを設定し、ボタンをクリック
    Call IEValueSet(objIE, idElement,uid)
    Call IEValueSet(objIE, passElement,pass)
    Call IEImageClick(objIE, loginElement)

    'IEを待機
    Call IEWait(objIE)

    '5秒停止
    Call WaitFor(2)

    'IE終了
    objIE.Quit

    Set objIE = Nothing
End Sub

'画像をクリックする関数
' タグの名前が「IMG」でリンク先が引数のimageSrcと一致している場合のみクリック
Public Function IEImageClick(ByRef objIE, imageSrc)
    Dim objImage
    For Each objImage In objIE.Document.getElementsByTagName("IMG")
        If InStr(objImage.src, imageSrc) <> 0 Then
            objImage.Click
            Exit For
        End If
    Next
End Function


'値を設定する関数
Public Function IEValueSet(ByRef objIE, id, value)
    Dim obj
    Set obj = objIE.Document.getElementById(id)
    obj.value = value
End Function

'ボタンをクリックする関数
Public Function IEButtonClick(ByRef objIE, id)
    Dim objbutton
    'button要素をコレクションとして取得
    Set objbutton = objIE.document.getElementById(id)
    objbutton.click
End Function


'IEを待機する関数
Function IEWait(ByRef objIE)
    Do While objIE.Busy = True Or objIE.readyState <> 4
       WScript.Sleep 1000
    Loop
End Function

'指定した秒だけ停止する関数
Function WaitFor(ByVal second)
    Dim count

For count = 1 To second
WScript.Sleep 1000
Next

End Function

複数列をもつTreeViewについて

以前の記事でも書いた複数列を持つTreeViewについて、独自でいろいろと改良したコントロールをdllにして、プロジェクトに組み込んでいたが、dllでの利用がやはりなれないので、シンプルにしたものを作成した。(MultiColumnTreeView)
以前の記事:https://inakaprogrammer.blogspot.com/2017/10/treelistview.html

(参照元:https://www.codeproject.com/Articles/24973/TreeListView)




まずは展開・非展開のToggleButtonをよく見る「+」「-」へ変更している。
あとは、カーソルのあるテキストボックスの背景色を変更するために、独自コントロールを作成し、DataTemplateとして指定できるようにしている。状況によってはコンボボックスやDatePickerなどを設置できるようにするべきかもしれない。

別プロジェクトとしてdllを作成するほどではないので、下記のサンプルプロジェクトの中では、TreeListViewフォルダをプロジェクト内に作成し、そのフォルダを別プロジェクトに移して、名前空間を修正したうえで、App.xamlに下記のソースを追加すれば使えるようになるはず。

<Application x:Class="MultiColumnTreeView.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:MultiColumnTreeView"
             StartupUri="MainWindow.xaml">
    <Application.Resources>

        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="TreeListView/TreeListViewStyle.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>

    </Application.Resources>
</Application>


各画面での利用例

       <TreeListView:TreeListView x:Name="treeView" Background="White" AllowsColumnReorder="True" ItemsSource="{Binding TreeRoot}" >
            <TreeListView:TreeListView.ItemTemplate>
                <HierarchicalDataTemplate ItemsSource="{Binding Children}" />
            </TreeListView:TreeListView.ItemTemplate>

            <TreeListView:TreeListView.Columns>

                <GridViewColumn Header="分類" Width="200">
                    <GridViewColumn.CellTemplate>
                        <DataTemplate>
                            <!-- Marginで左右から-6pxとしないとHearder部のタイトルとラインの位置がずれる.GridViewの既知の不具合 -->
                            <Border BorderBrush="Black"  BorderThickness="0,0,0.5,0" Margin="-6,0,-6,0">
                                <DockPanel   >
                                    <!--TreeListViewExpanderは必ず最初の列に設置-->
                                    <TreeListView:TreeListViewExpander Focusable="False"/>
                                    <TreeListViewSubControls:TreeListItemTextBox Text="{Binding Text}" VerticalContentAlignment="Center" VerticalAlignment="Stretch" Margin="2,2,2,2"
                                                                  HorizontalAlignment="Stretch" BorderThickness="0" Opacity="1"  TabIndex="1" />
                                </DockPanel>
                            </Border>
                        </DataTemplate>
                    </GridViewColumn.CellTemplate>
                </GridViewColumn>

                <GridViewColumn Header="期限" Width="80">
                    <GridViewColumn.CellTemplate>
                        <DataTemplate>
                            <!-- Marginで左右から-6pxとしないとHearder部のタイトルとラインの位置がずれる.GridViewの既知の不具合 -->
                            <Border BorderBrush="Black"  BorderThickness="0,0,0.5,0" Margin="-6,0,-6,0">
                                <DockPanel   >
                                    <TreeListViewSubControls:TreeListItemTextBox Text="{Binding Limit}" VerticalContentAlignment="Center" VerticalAlignment="Stretch" Margin="2,2,2,2"
                                                                  HorizontalAlignment="Stretch"  HorizontalContentAlignment="Center" BorderThickness="0" Opacity="1"  TabIndex="1" />
                                </DockPanel>
                            </Border>
                        </DataTemplate>
                    </GridViewColumn.CellTemplate>
                </GridViewColumn>

            </TreeListView:TreeListView.Columns>
        </TreeListView:TreeListView>


サンプルプロジェクト
<DownLoad>


PowerShellでWPFアプリケーションをBindingするときの注意点

 1.PowerShellでWPFアプリケーションのBindingについて マニアックだけどPowerShellでWPFアプリケーションを作っている。INotifyChangedを実装したいけど、PowerShellにはgetterやsetterがないので通知が発行できない。 そ...